Spring 中的资源获取

Spring 中的资源获取

参考博客

SpringBoot--本地文件_51CTO博客_springboot下载文件到本地

Spring Resource和ResourceLoader源码解析_nju.拈花的博客-CSDN博客

【小家Spring】资源访问利器---Spring提供的Resource接口以及它的常用子类源码分析_方向盘(YourBatman)的博客-CSDN博客

【小家Spring】资源访问利器---Spring使用ResourceLoader接口便捷的获取资源(ResourcePatternResolver、ResourceLoaderAware)_方向盘(YourBatman)的博客-CSDN博客

简单的源码分析

Resource

全包名是 org.springframework.core.io.Resource

Resource 继承 InputStreamSource 接口

InputStreamSource 接口也是 Spring 定义的,全包名为 org.springframework.core.io.InputStreamSource,用于描述一个可以作为 InputStream 的源的对象。

从底层资源 (如文件或类路径资源) 的实际类型进行抽象,得出的顶层的资源描述接口,即:Resource 是对多种不同资源的一个统一封装。如果资源物理存在,则可以为这个资源打开 InputStream 读取流,但对于某些类型的资源只能返回 URL 或 File 对象。实际的行为是特定于实现的。

关于文件或类路径资源介绍,参考《Java 中的常见路径和资源获取.md》

Resource 接口中几个比较常用方法

Resource 的默认实现如下:

其中最常用的,还是 AbstractResource 这个分支下的类,比如 UrlResource,FileUrlResource,ClassPathResource,FileSystemResource,这个我们接下来细研究其使用。

AbstractResource

Resource 接口实现的基础抽象类,包含了一些预先实现的典型的行为,比如

AbstractFileResolvingResource

继承 AbstractResource,主要用于将 URL 解析为 File 对象的资源,例如 UrlResource 或 ClassPathResource。

根据 URL 的协议,比如 file: 协议,vfs: 协议,将资源进行相应的解析,

在 ResourceUtils 中具体的支持的解析协议,

ResourceUtils 我们后面会经常用到,是资源处理的常用工具类。

重点看看 exists 方法的实现,UrlResource 和 FileUrlResource 都继承了这个方法来判断资源是否存在,ClassPathResource 则是重写了这个方法

每次使用 Resource 资源之前都应该调用 exists 方法检查一下

@Override
public boolean exists() {
    try {
        // 获取资源的URL
        URL url = getURL();
        // 如果URL指向的是文件系统下的资源,直接判断文件是否存在即可
        if (ResourceUtils.isFileURL(url)) {
            // Proceed with file system resolution
            return getFile().exists();
        }
        else {
            // 如果URL指向的是常规的HTTP地址,则直接发起一个请求,看响应结果
            // Try a URL connection content-length header
            URLConnection con = url.openConnection();
            customizeConnection(con);
            HttpURLConnection httpCon = (con instanceof HttpURLConnection ? (HttpURLConnection) con : null);
            if (httpCon != null) {
                // HTTP方法为 HEAD,只获取响应头信息
                httpCon.setRequestMethod("HEAD");
                // 检查响应,看是不是通的
                int code = httpCon.getResponseCode();
                if (code == HttpURLConnection.HTTP_OK) {
                    return true;
                }
                else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
                    return false;
                }
            }
            if (con.getContentLengthLong() > 0) {
                return true;
            }
            if (httpCon != null) {
                // No HTTP OK status, and no content-length header: give up
                httpCon.disconnect();
                return false;
            } else {
                // Fall back to stream existence: can we open the stream?
                getInputStream().close();
                return true;
            }
        }
    }
    catch (IOException ex) {
        return false;
    }
}

UrlResource

继承 AbstractFileResolvingResource,这是对 java.net.URL 资源定位器(比如我们常见的 HTTP 的 URL 地址)的 Resource 封装,支持解析为 URL 对象(UrlResource#url),然后直接用 URL 对象的输入流来当作资源的输入流,因此可以用来访问 URL 对象本身可以正常访问的任意对象,因此,它支持 http、https、file、ftp、jar 等协议。其 getInputStream() 方法实现如下:

@Override
public InputStream getInputStream() throws IOException {
    URLConnection con = this.url.openConnection();
    customizeConnection(con);
    try {
        // 直接返回内部的URL对象的输入流
        return con.getInputStream();
    }
    catch (IOException ex) {
        // Close the HTTP connection (if applicable).
        if (con instanceof HttpURLConnection) {
            ((HttpURLConnection) con).disconnect();
        }
        throw ex;
    }
}

当使用 file: 协议的时候,也可以解析为 File 对象。此时我们可以使用其子类 FileUrlResource

UrlResource 支持各种方式构造:

实践

经过 UrlResource 的封装,只要知道一个 HTTP 连接或者一个 FTP 链接,就可以直接通过 URL 路径获取 URL 资源,真的非常方便,

UrlResource 可以用来判断 URL 是否能通

public static void main(String[] args) throws IOException {
    //SpringApplication.run(ResourceApplication.class, args);
    // 直接通过URL路径获取URL资源
    UrlResource urlResource = new UrlResource("http://www.baidu.com");
    boolean pageExists = urlResource.exists();
    System.out.println(pageExists);
    if (pageExists) {
        //如果页面存在,直接通过输入流获取页面的内容
        InputStream webPage = urlResource.getInputStream();
        StringBuilder sb = new StringBuilder();
        int len = -1;
        byte[] buff = new byte[1024];
        while ((len = webPage.read(buff)) > 0) {
            String data = new String(buff, 0, len);
            sb.append(data);
        }
        System.out.println(sb);
        // 流用完了一定要关闭
        webPage.close();
    }
    // UrlResource 可以用来判断URL是否能通
    UrlResource blogPage = new UrlResource("https://xiashuo.xyz/");
    boolean blogExists = blogPage.exists();
    System.out.println(blogExists);
}

FileUrlResource

UrlResource 的子类,主要用于进行将 URL 解析成文件(File 实例)时的工作,而且通过实现 WritableResource 接口,可以提供 URL 对应的文件的 getOutputStream,方便用户对文件进行修改。此外,没有重写 UrlResource 的 getInputStream 的方法。

这是 DefaultResourceLoader 在解析 URL 路径为 file:… 的资源时使用的资源类。而且因为 FileUrlResource 实现了 WritableResource,所以允许进行类型转化,转换为 WritableResource 类型。

而当我们需要直接从 File 实例或 NIO 的 Path 实例构造资源对象时,请直接使用 FileSystemResource,这个我们后面会提到。

此外,FileUrlResource 提供了两种构建方式,

实践

注意相对路径的起点是当前应用的工作目录。

修改当前 main 方法的 Run/Debug configuration 的 Working Directory 为当前模块的根路径,如果不设置,默认的工作目录是当前模块所在的项目的根路径

在工作目录也就是当前模块的根路径下创建文件 config.properties,不写入任何内容,空着

执行 main 方法

public static void main(String[] args) throws IOException {
    // 路径不需要带前缀 file:
    FileUrlResource fileUrlResource = new FileUrlResource("./config.properties");
    boolean fileExists = fileUrlResource.exists();
    if (fileExists) {
        System.out.println(fileUrlResource.getFile().getCanonicalFile());
        OutputStream outputStream = fileUrlResource.getOutputStream();
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
        outputStreamWriter.write("application.name = ResourceTest");
        outputStreamWriter.flush();
        // 流用完了一定要关闭
        outputStreamWriter.close();
        outputStream.close();
    }

}

写入之后,文件内容为

application.name = ResourceTest

ClassPathResource

继承 AbstractFileResolvingResource,这是类路径下的资源的 Resource 实现。

可使用给定的 ClassLoader 或给定的 Class 来加载资源。如果类路径资源位于文件系统中,则支持将其解析为 java.io.File,但对于类路径中的第三方 jar 包中的资源,通过 ClassPathResource 依然可以获取,但是无法将其转化为 java.io.File 对象。即 getFile() 返回 null

通过 ClassLoader 和 Class 来加载类路径下的资源的实践,我们在《Java 中的常见路径和资源获取.md》中尝试过

构造函数:

值得注意的是在只指定路径字符串和类加载器的时候,如果路径字符串的第一个字符是 /,则去掉,因为使用类加载器获取资源的时候,以 / 开头会返回 null,这个我们在《Java 中的常见路径和资源获取.md》中都证实过。Spring 帮我们屏蔽了这个问题。

public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
    Assert.notNull(path, "Path must not be null");
    String pathToUse = StringUtils.cleanPath(path);
    // 使用类加载器获取资源的时候,以`/`开头的路径会返回null,这个我们在《Java中的常见路径和资源获取.md》中都证实过。Spring帮我们屏蔽了这个问题。
    if (pathToUse.startsWith("/")) {
        pathToUse = pathToUse.substring(1);
    }
    this.path = pathToUse;
    this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}

将路径字符串解析为 URL 和通过路径字符串获取资源的时候,默认先用 Class 实例进行操作,然后再用类加载器。注意这一点

@Nullable
protected URL resolveURL() {
    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;
    }
}
@Override
public InputStream getInputStream() throws IOException {
    InputStream is;
    if (this.clazz != null) {
        is = this.clazz.getResourceAsStream(this.path);
    }
    else if (this.classLoader != null) {
        is = this.classLoader.getResourceAsStream(this.path);
    }
    else {
        is = ClassLoader.getSystemResourceAsStream(this.path);
    }
    if (is == null) {
        throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
    }
    return is;
}

可以看到,ClassPathResource 对 getInputStream 的实现,都是委托给了 Clazz.getResource 和 ClassLoader.getResource。

通过 ClassLoader 和 Class 来加载类路径下的资源的实践,我们在《Java 中的常见路径和资源获取.md》中尝试过。

实践

在 src\main\resources 下的 application.properties 中编辑内容

spring.application.name=xiashuoDemo

然后编写测试类

public static void main(String[] args) throws IOException {
    ClassPathResource resource = new ClassPathResource("application.properties");
    // 可以将获取到的资源转化为Fiel对象
    File file1 = resource.getFile();
    System.out.println(file1.getCanonicalPath());
    boolean exists = resource.exists();
    System.out.println(exists);
    if (exists) {
        //如果页面存在,直接通过输入流获取页面的内容
        InputStream file = resource.getInputStream();
        StringBuilder sb = new StringBuilder();
        int len = -1;
        byte[] buff = new byte[1024];
        while ((len = file.read(buff)) > 0) {
            String data = new String(buff, 0, len);
            sb.append(data);
        }
        System.out.println(sb);
        // 流用完了一定要关闭
        file.close();
    }
    // --- 获取第三方jar包中的资源 虽然获取的是乱码,但是是可以获取的
    ClassPathResource thirdPartyJarSource = new ClassPathResource("org/springframework/asm/AnnotationWriter.class");
    // 无法将获取到的资源转化为Fiel对象
    //File file2 = thirdPartyJarSource.getFile();
    //System.out.println(file2.getCanonicalPath());
    boolean thirdPartyJarSourceExists = thirdPartyJarSource.exists();
    System.out.println(thirdPartyJarSourceExists);
    if (thirdPartyJarSourceExists) {
        //如果页面存在,直接通过输入流获取页面的内容
        InputStream classFile = thirdPartyJarSource.getInputStream();
        StringBuilder sb = new StringBuilder();
        int len = -1;
        byte[] buff = new byte[1024];
        while ((len = classFile.read(buff)) > 0) {
            String data = new String(buff, 0, len);
            sb.append(data);
        }
        System.out.println(sb);
        // 流用完了一定要关闭
        classFile.close();
    }
}

可以正常输出

E:\IDEAProject\Spring5Framework\Resource\target\classes\application.properties
true
spring.application.name=xiashuoDemo
true
省略乱码

FileSystemResource

继承 AbstractResource,跟 FileUrlResource 类似,不过 FileUrlResource 是基于 URL 的,而 FileSystemResource 是之间基于 File 和 Path。主要以 NIO 的方式提供流

这是对 java.io.File 和 java.nio.file.Path 的 Resource 封装,对应着一个文件系统中的一个文件,支持解析为 File 和 URL,实现的 WritableResource 接口。

FileUrlResource 也实现了 WritableResource 接口。

我感觉 FileSystemResource 得使用,是没有 UrlResource 的使用广泛的

构造函数:

对 getInputStream 和 getOutputStream,都是直接传入 Path 打开 NIO 流。可以看到,FileSystemResource 是基于 NIO 的。

@Override
public OutputStream getOutputStream() throws IOException {
    return Files.newOutputStream(this.filePath);
}

@Override
public InputStream getInputStream() throws IOException {
    try {
        return Files.newInputStream(this.filePath);
    }
    catch (NoSuchFileException ex) {
        throw new FileNotFoundException(ex.getMessage());
    }
}
实践

跟前面的使用大同小异

public static void main(String[] args) throws IOException {
    FileSystemResource fileSystemResource = new FileSystemResource("config.properties");
    boolean fileExists = fileSystemResource.exists();
    if (fileExists) {
        System.out.println(fileSystemResource.getFile().getCanonicalFile());
        InputStream inputStream = fileSystemResource.getInputStream();
        StringBuilder sb = new StringBuilder();
        int len = -1;
        byte[] buff = new byte[1024];
        while ((len = inputStream.read(buff)) > 0) {
            String data = new String(buff, 0, len);
            sb.append(data);
        }
        System.out.println(sb);
        // 流用完了一定要关闭
        inputStream.close();
        OutputStream outputStream = fileSystemResource.getOutputStream();
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
        outputStreamWriter.write("application.name = ResourceTest");
        outputStreamWriter.flush();
        // 流用完了一定要关闭
        outputStreamWriter.close();
        outputStream.close();
    }
}

InputStreamResource

InputStreamResource 把一个 InputStream 封装为 Resource,只有在没有其他特定的 Resource 实现适用时才应该使用此封装。在可能的情况下,首选 ByteArrayResource 或任何基于文件的 Resource 实现,比如 FileUrlResource 和 FileSystemResource。

对 getInputStream 的实现就是返回其封装的 InputStream,很简单

@Override
public InputStream getInputStream() throws IOException, IllegalStateException {
    if (this.read) {
        throw new IllegalStateException("InputStream has already been read - " +
                "do not use InputStreamResource if a stream needs to be read multiple times");
    }
    this.read = true;
    return this.inputStream;
}

懒得实践了

ByteArrayResource

把一个字节数组封装为 Resource。用于从任何给定的字节数组加载内容,而不必求助于一次性使用的 InputStreamResource。对于从本地内容创建邮件附件特别有用,因为 JavaMail 需要能够多次读取流。

对 getInputStream 的实现就是为字节数组创建一个 ByteArrayInputStream

@Override
public InputStream getInputStream() throws IOException {
    // 直接使用字节数组构造一个字节数组读取流
    return new ByteArrayInputStream(this.byteArray);
}

懒得实践了

ServletContextResource - web 项目专用

继承 AbstractFileResolvingResource,对 ServletContext 中的资源的 Resource 实现,用于解析 web 应用程序根目录中的相对路径。

这个 web 应用根路径,一般就是指 src/main/webapp,但是在引入 web 启动器的 SpringBoot 项目中,默认是没有配置 web 应用程序根目录的,毕竟 web.xml 都没有了。当然,我们可以手动配置。

始终支持流访问和 URL 访问,但只允许在 web 应用程序在 war:exploded 时进行 java.io.File 访问。

构造函数,只有一个

注意,传入的 path 参数就算不以 / 开头,最后也会在开头强制加上 /。

public ServletContextResource(ServletContext servletContext, String path) {
    // check ServletContext
    Assert.notNull(servletContext, "Cannot resolve ServletContextResource without ServletContext");
    this.servletContext = servletContext;

    // check path
    Assert.notNull(path, "Path is required");
    String pathToUse = StringUtils.cleanPath(path);
    // 强制路径以 / 开头
    if (!pathToUse.startsWith("/")) {
        pathToUse = "/" + pathToUse;
    }
    this.path = pathToUse;
}

我们看 exists 方法和 isFile 方法,发现实际上都是委托给 ServletContext 的 getResource 方法,

ServletContext#getResource 的注释:

返回给定路径映射到的资源的 URL

给定的路径必须以 / 开头,/ 会被解释为相对于(以之为起点)当前 web 应用的根目录(对应这 Maven 目录结构中的 src/main/webapp),或相对于 web 应用的根目录的 /WEB-INF/lib 目录中的 JAR 文件的 /META-INF/resources 目录的相对路径。在搜索 /WEB-INF/lib 中的任何 JAR 文件之前,该方法将首先搜索 web 应用程序的根目录以获得请求的资源。搜索 /WEB-INF/lib 中的 JAR 文件的顺序是未定义的。

这个方法使得 servlet 容器赋予了任何来源的 servlet 都可以访问资源的能力。资源可以位于本地或远程文件系统、数据库或 .war 文件中。

....

这个方法的用途与 java.lang.Class.getResource 不同。Class.getResource 基于类加载器查找资源。ServletContext#getResource 不使用类装入器。

@Override
public boolean exists() {
    try {
        URL url = this.servletContext.getResource(this.path);
        return (url != null);
    }
    catch (MalformedURLException ex) {
        return false;
    }
}

@Override
public boolean isFile() {
    try {
        URL url = this.servletContext.getResource(this.path);
        if (url != null && ResourceUtils.isFileURL(url)) {
            return true;
        }
        else {
            return (this.servletContext.getRealPath(this.path) != null);
        }
    }
    catch (MalformedURLException ex) {
        return false;
    }
}

getInputStream 方法也是如此,委托给 ServletContext 的 getResourceAsStream 方法,

@Override
public InputStream getInputStream() throws IOException {
    InputStream is = this.servletContext.getResourceAsStream(this.path);
    if (is == null) {
        throw new FileNotFoundException("Could not open " + getDescription());
    }
    return is;
}
实践

web 应用程序根目录结构:

测试代码

@RequestMapping("/getResource")
public String HttpSession(HttpServletRequest request) throws IOException {
    ServletContext application = request.getServletContext();
    // 默认的路径起点是 src/main/webapp
    ServletContextResource servletContextResource = new ServletContextResource(application, "/WEB-INF/templates/index.html");
    File file = servletContextResource.getFile();
    boolean exists = servletContextResource.exists();
    // 省略对文件的读取
    //servletContextResource.getInputStream();
    return "target-application";
}

ResourceUtils - Resource 工具类

其中地工具方法主要分两类,一类是判断当前 URL 指向的资源是不是某种类型的资源,

一类是获取 URL 中的具体资源,

ResourceLoader - 资源加载器

ResourceLoader 是一个用于加载资源 (例如,类路径资源 ClassPathResource 或文件系统资源 FileUrlResource) 的策略接口。

org.springframework.context.ApplicationContext 也就是我们常说的应用上下文需要提供这个加载资源的功能,而且还需要支持 org.springframework.core.io.support.ResourcePatternResolver 即根据模式匹配资源的功能。

而 DefaultResourceLoader 是一个独立的 ResourceLoader 实现,可以在 ApplicationContext 之外使用,也可以由 ResourceEditor 使用。

当在 ApplicationContext 中运行时,可以使用特定的上下文的 ResourceLoader 实现从 String 中填充 Resource 和 Resource[] 类型的 Bean 属性。

只有两个接口方法

有了 ResourceLoader,程序员在获取资源的时候,就可以不去计较使用哪一种类型的 Resource 的实现,直接使用 ResourceLoader.getResource(),传入正确的资源路径,即可获取 Resource。

我们在进行本地资源查找的时候,实际上也就这三种情况,

其他的比如网络资源,这个不在本地资源查找的范畴之下,可以直接通过对应的资源实现类来处理。比如通过 URLresource 判断网址是否可以 ping 通

具体实现类如下:

可以看到接口实现主要分两类,一类是 DefaultResourceLoader,我们就看 DefaultResourceLoader 怎么用,一类是 ResourcePatternResolver,我们看看 PathMatchingResourcePatternResolver 和 ApplicationContext 怎么用。

DefaultResourceLoader

DefaultResourceLoader 是 ResourceLoader 接口的默认实现。这也是我们在源码中,见到的最多的 ResourceLoader 实现类。

在 ResourceEditor 中有所使用,也是 org.springframework.context.support.AbstractApplicationContext 的父类。当然也可以单独使用。

如果资源位置是一个 URL,将返回一个 UrlResource,如果它是非 URL 路径或 classpath: 开头的 URL,则返回一个 ClassPathResource。

protocolResolvers 字段,是一个有序的 Set,表示协议解析链,用于在 getResource 中进行自定义的协议解析。元素类型为 ProtocolResolver。

ProtocolResolver 是一个函数式接口,作用是对特定协议下的资源进行处理,我们可以通过在 DefaultResourceLoader 中注册 ProtocolResolver 来实现对自定义协议下的资源的处理。

private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

如果前面的 ProtocolResolver 成功解析,则直接返回不再解析,所以协议解析链中的协议解析器的顺序很重要,具体的使用看实践小节

继续看 DefaultResourceLoader#getResource 方法,

资源位置的解析顺序是:

  1. 先进行协议解析链解析

  2. 然后进行类路径解析,在类路径中查找

  3. 最后再进行 URL 解析

  4. 如果都不是,还是默认进行类路径解析,在类路径中查找

前面的解析成功,就会直接返回,后面的步骤就不再执行

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

    // 先通过协议解析链解析资源地址
    for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
        Resource resource = protocolResolver.resolve(location, this);
        if (resource != null) {
            // 如果成功解析,则直接返回不再解析,所以协议解析链中的协议解析器的顺序很重要
            return resource;
        }
    }

    // 以/开头,最终还是返回 ClassPathResource
    if (location.startsWith("/")) {
        return getResourceByPath(location);
    }
    // 直接以 classpath: 开头的路径,最终返回 ClassPathResource
    else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    }
    else {
        try {
            // 尝试将资源地址转化为URL
            // Try to parse the location as a URL...
            URL url = new URL(location);
            // 如果成功将资源地址转化为URL,则进一步判断 是不是文件协议下的url,如果是,则返回FileUrlResource,不是,则返回 UrlResource 
            return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
        }
        catch (MalformedURLException ex) {
            // 如果无法将资源地址转化为URL,尝试在类路径下解析
            // No URL -> resolve as resource path.
            return getResourceByPath(location);
        }
    }
}
实践
public static void main(String[] args) {
    // 测试单独使用
    DefaultResourceLoader loader = new DefaultResourceLoader();
    loader.addProtocolResolver((location, resourceLoader) -> {
        // 当使用自定义协议的时候,是无法直接将url字符串转化为 URL对象的
        // 因为虽然可以获取协议名,但是URL.getURLStreamHandler 在根据协议名获取流处理器的时候返回结果为null,抛出异常 MalformedURLException
        URI uri = null;
        try {
            uri = ResourceUtils.toURI(location);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        if (uri == null) {
            return null;
        }
        String protocol = uri.getScheme();
        if (protocol == null || !"ABC".equals(protocol.toUpperCase())) {
            return null;
        }
        // 开始处理 ABC 协议的资源路径
        // 将自定义协议转化为 http协议
        String newLocation = location.replaceAll("abc", "http");
        Resource resource;
        try {
            resource = new UrlResource(newLocation);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        return resource;
    });
    // 获取自定义协议下的资源
    Resource myProtocolResource = loader.getResource("abc://www.baidu.com");
    boolean myProtocolResourceExist = myProtocolResource.exists();
    System.out.println("自定义协议资源是否存在:" + myProtocolResourceExist);
    // 获取类路径下的资源
    Resource classpathResource = loader.getResource("classpath:./application.properties");
    boolean classpathResourceExist = classpathResource.exists();
    System.out.println("类路径下的资源是否存在:" + classpathResourceExist);
    // 获取文件系统下的资源
    Resource fileResource = loader.getResource("file:./config.properties");
    boolean fileResourceExist = fileResource.exists();
    System.out.println("文件系统下的资源是否存在:" + fileResourceExist);
    // 获取类路径下的资源
    Resource onlyNameLocationResource = loader.getResource("application.properties");
    boolean onlyNameLocationResourceExist = onlyNameLocationResource.exists();
    System.out.println("不带任何协议头,则默认在类路径下查找,资源是否存在:" + onlyNameLocationResourceExist);
}

输出

自定义协议资源是否存在:true
类路径下的资源是否存在:true
文件系统下的资源是否存在:true
不带任何协议头,则默认在类路径下查找,资源是否存在:true

注意

当使用自定义协议的时候,是无法直接将 url 字符串转化为 URL 对象的,因为虽然可以获取协议名,但是 URL.getURLStreamHandler 在根据协议名获取流处理器的时候返回结果为 null,抛出异常 MalformedURLException

注意相对路径

FileUrlResource 的 exists 方法继承自 AbstractFileResolvingResource(具体细节看 AbstractFileResolvingResource 小节),最终会直接获取 URL 指向的 File 实例,通过检查这个 File 实例指向的文件是否物理存在来判断资源存不存在,如果资源路径是 file:./ 开头,这是一个相对路径,因为以相对路径创建 File 实例的时候相对路径的起点就是当前 MainClass 的工作目录,因此,这个相对路径最终会以 System.getProperty("user.dir") 返回的路径为相对路径的起点查找文件,

调用栈:

ResourceUtils#getFile 方法

public static File getFile(URL resourceUrl, String description) throws FileNotFoundException {
    Assert.notNull(resourceUrl, "Resource URL must not be null");
    if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) {
        throw new FileNotFoundException(
                description + " cannot be resolved to absolute file path " +
                "because it does not reside in the file system: " + resourceUrl);
    }
    try {
        // 最终还是通过 File 来获取文件
        // 如果路径为相对路径,File文件就会以 System.getProperty("user.dir") 为相对路径的起点查找文件
        return new File(toURI(resourceUrl).getSchemeSpecificPart());
    }
    catch (URISyntaxException ex) {
        // Fallback for URLs that are not valid URIs (should hardly ever happen).
        return new File(resourceUrl.getFile());
    }
}

File 实例的创建跟工作目录的关系,看《Java 中的常见路径和资源获取.md》

ResourcePatternResolver

继承 ResourceLoader 接口,增加了方法:

Resource[] getResources(String locationPattern) throws IOException;

在 ResourceLoader 的基础上更进一步,根据资源位置模板 (例如 ant 样式的路径模式) 解析出多个 Resource 对象。我们可以通过指定位置模板,一次性获取所有匹配路径模板的资源。

实现类:

ApplicationContext 实现了 ResourcePatternResolver,可在运行时通过 ResourceLoaderAware 获得。

PathMatchingResourcePatternResolver 是一个独立的实现,可在 ApplicationContext 之外使用,ResourceArrayPropertyEditor 也使用它来填充资源数组 bean 属性。

可以与任何类型的位置模板一起使用例如,/WEB-INF/*-context.xml。然而,输入的位置模版必须与 ResourcePatternResolver 的实现相匹配。ResourcePatternResolver 接口只是指定转换方法,而不是指定特定的模式格式。

PathMatchingResourcePatternResolver

实现 ResourcePatternResolver 接口,能够将指定的资源位置路径解析为一个或多个匹配的资源(Resource 对象)。资源位置路径可以是到目标资源的一对一映射的简单路径,也可以包含特殊的 classpath*: 前缀和/或内部 ant 风格的正则表达式 (使用 Spring 的 AntPathMatcher 工具进行匹配)。后两者都是有效的通配符。

AntPathMatcher 的匹配规则,看《UrlPathHelper+PathMatcher 简单解析.md》

匹配方式(简单看看即可):

接下来解析一下 PathMatchingResourcePatternResolver 的源码

主要有两个核心属性

// 本质上还是要委托给 resourceLoader 进行资源的查找,比如 getResource 方法就是委托给 resourceLoader 的 getResource方法
// 这个属性可以在构造函数中传入,也可以不传入,此时默认为 DefaultResourceLoader
private final ResourceLoader resourceLoader;
// 用于资源的路径匹配 具体用法看 看《UrlPathHelper+PathMatcher简单解析.md》
// 默认初始化为 AntPathMatcher,当然也可以通过 setPathMatcher方法进行更改,不过一般都不会修改,没有必要
private PathMatcher pathMatcher = new AntPathMatcher();

getResource 方法的实现实际上就是委托给 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)) {
        // 把 CLASSPATH_ALL_URL_PREFIX 后面那部分截取出来,看看是否是模版(包含通配符,比如 * 和 ?  就是模版,否则不是)
        if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
            // a class path resource pattern
            // 是模板 ,那就交给这个方法,这个是个核心方法   这里传入的是locationPattern
            return findPathMatchingResources(locationPattern);
        }
        else {
            // all class path resources with the given name
            // 如果不是模板,那就完全匹配。去找所有的类路径中的资源,path匹配上就行
            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    }

    // 不是以`classpath*:`打头的
    else {
        // 支持到tomcat的war:打头的方式
        // 注意,如果 locationPattern 不以任何协议开头(不包含 : ),就是一个常规的路径,比如config/aaa/bbb/*.txt,那么,此时的prefixEnd就是0,依然有效
        int prefixEnd = (locationPattern.startsWith("war:") ?  locationPattern.indexOf("*/") + 1 :
                locationPattern.indexOf(':') + 1);
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            return findPathMatchingResources(locationPattern);
        }

        // 如果啥都不打头,那就当作一个正常的处理,委托给ResourceLoader直接去处理
        else {
            // a single resource with the given name
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

// 通过 ClassLoader 中查找所有的的路径匹配的资源,委托给 doFindAllClassPathResources
protected Resource[] findAllClassPathResources(String location) throws IOException {
    String path = location;
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    Set<Resource> result = doFindAllClassPathResources(path);
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved classpath location [" + location + "] to resources " + result);
    }
    return result.toArray(new Resource[0]);
}

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    Set<Resource> result = new LinkedHashSet<>(16);
    ClassLoader cl = getClassLoader();
    // 直接委托给 ClassLoader 的 getResources 方法,返回 URL
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    // 依次遍历所有的 URL,构建成 UrlResource 添加到返回的Set中
    while (resourceUrls.hasMoreElements()) {
        URL url = resourceUrls.nextElement();
        // convertClassLoaderURL 实际上就是根据 URL构建了 UrlResource
        result.add(convertClassLoaderURL(url));
    }
    if (!StringUtils.hasLength(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;
}

// 根据Pattern去匹配资源,处理通配符的匹配
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    // 确定以locationPattern进行资源匹配时的根文件夹。
    // 比如locationPattern=classpath:META-INF/spring.factories  得到classpath*:META-INF/
    // 若是classpath*:META-INF/ABC/*.factories 得到的就是 classpath*:META-INF/ABC/
    // 简单的说,就是截取第一个不是patter的地方的前半部分
    String rootDirPath = determineRootDir(locationPattern);
    // 以locationPattern进行资源匹配时的根文件夹的后半部分,也就是包含通配符的部分,比如根据前面的例子就是:*.factories
    String subPattern = locationPattern.substring(rootDirPath.length());

    // 这个递归就厉害了,继续调用了getResources("classpath*:META-INF/")方法,因为其中包含通配符,所以会走 findAllClassPathResources方法,或者 getResourceLoader().getResource
    // 相当于把该文件夹匹配的所有的资源(注意:可能会比较多的),最后在和patter匹配即可
    // 比如此处:只要jar里面有META-INF目录的  都会被匹配进来
    Resource[] rootDirResources = getResources(rootDirPath);
    Set<Resource> result = new LinkedHashSet<>(16);
    // 遍历每一个根文件夹处理根文件夹下的资源
    for (Resource rootDirResource : rootDirResources) {
        // resolveRootDirResource是留给子类去复写的。但是Spring没有子类复写此方法,默认实现是啥都没做
        rootDirResource = resolveRootDirResource(rootDirResource);
        URL rootDirUrl = rootDirResource.getURL();

        // 这个if就一般不看了  是否为了做兼容
        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协议(JBoss)
        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
        }

        // 是否是jar文件或者是jar资源(显然大多数情况下都是此情况)
        else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
            // 把rootDirUrl, subPattern都交给doFindPathMatchingJarResources去处理
            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
        }

        // 不是Jar文件(那就是本工程里字的META-INF目录)
        // 那就没啥好说的,直接给个subPattern去匹配吧,一般都是进行文件资源的查找
        else {
            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
        }
    }

    // 最终转换为数组返回。 注意此处的result是个set,是有去重的效果的
    return result.toArray(new Resource[0]);
}

// 确定资源匹配的根文件夹。
protected String determineRootDir(String location) {
    // 如果以协议开头,那就从协议后面开始算,如果不以协议开头,那就直接从0也就是第一个字符开始算
    int prefixEnd = location.indexOf(':') + 1;
    // rootDirEnd 一开始就是到 location 的末尾
    int rootDirEnd = location.length();
    // 不断从后往前缩短路径直到不包含通配符
    while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
        // 如果截取的字符串依然包含通配符,则从后往前缩短一层路径
        // PS :每层路径通过 / 隔开
        rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
    }
    if (rootDirEnd == 0) {
        rootDirEnd = prefixEnd;
    }
    // 注意,是从0开始,而不是从 prefixEnd 开始,也就是(如果有的话)也会带上协议,比如file:、classpath:
    return location.substring(0, rootDirEnd);
}

protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern) {
    ... 
    // 这个方法就源码不细说了,有点长
    //Find all resources in jar files that match the given location pattern
    // 就是去这个jar里面去找所有的资源(默认利用Ant风格匹配)
    //此处用到了`java.util.jar.JarFile`、`ZipFile`、`java.net.JarURLConnection`等等
    // 路径匹配:getPathMatcher().match(subPattern, relativePath) 使用的此方法去校对
}

// Find all resources in the file system that match the given location pattern
// 简单的说,这个就是在我们自己的当前的文件系统里找
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
        throws IOException {

    // rootDir:最终是个绝对的路径地址,带盘符的。来代表META-INF这个文件夹
    File rootDir;
    try {
        rootDir = rootDirResource.getFile().getAbsoluteFile();
    } catch (IOException ex) {
        return Collections.emptySet();
    }
    // FileSystem  最终是根据此绝对路径 去文件系统里找
    return doFindMatchingFileSystemResources(rootDir, subPattern);
}

// 这个比较简单:就是把该文件夹所有的文件都拿出来dir.listFiles(),然后一个个去匹配
// 备注:子类`ServletContextResourcePatternResolver`复写了此方法~
protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
    if (logger.isDebugEnabled()) {
        logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
    }
    // 遍历根目录下的文件,和subPattern进行一一匹配
    Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
    Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
    for (File file : matchingFiles) {]
        // 最终用FileSystemResource把File包装成一个Resource
        result.add(new FileSystemResource(file));
    }
    return result;
}

简单分析一下过程,其实主要就分四种情况

现在来看看 findPathMatchingResources 的逻辑

  1. 首先获取资源查找路径的根路径(根路径不包含通配符,所以根路径实际上是固定路径)

  2. 然后获取资源查找路径中,根路径后面的带通配符的路径

  3. 查找所有根路径对应的资源

  4. 遍历每一个根路径,获取根路径下的所有资源(根据资源类型分情况)跟带通配符的路径进行匹配,匹配上的资源最终返回

实践
public static void main(String[] args) throws IOException {
    ResourceLoader resourceLoader = new DefaultResourceLoader();
    Resource resource = resourceLoader.getResource("classpath:META-INF/spring.factories");
    // 理论上应该是哟很多个的,但是这里只找到一个。
    // jar:file:/D:/Program-Dev/m2/repository_nexus_local/org/springframework/boot/spring-boot/2.7.5/spring-boot-2.7.5.jar!/META-INF/spring.factories
    System.out.println("----------------------------------- ResourceLoader -----------------------------------");
    System.out.println(resource.getURL());
    System.out.println(resource.exists());

    System.out.println("----------------------------------- ResourcePatternResolver -----------------------------------");
    PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
    // classpath:META-INF/spring.factories 表示只用找在一个jar包中找,找到即可返回,所以结果使一个
    // classpath*:META-INF/spring.factories 表示去所有的类路径下找,包括当前模块的编译输出路径和引用的jar包
    Resource[] resources0 = resourcePatternResolver.getResources("classpath:META-INF/spring.factories");
    System.out.println("start with classpath:"+resources0.length);
    Resource[] resources1 = resourcePatternResolver.getResources("classpath*:META-INF/spring.factories");
    System.out.println("start with classpath*:"+resources1.length);

    // 还能使用Ant风格进行匹配  太强大了:
    // 查看类路径下所有的`META-INF/*.factories` 能匹配上的所有文件
    Resource[] resources = resourcePatternResolver.getResources("classpath*:META-INF/*.factories");
    System.out.println("classpath* plus ant-style path: "+resources.length);
    // 查看类路径下指定包下的所有class类,比如我的
    Resource[] resourcesAll = resourcePatternResolver.getResources("classpath*:xyz/xiashuo/**/*.class");
    System.out.println("classpath* plus ant-style path: "+resourcesAll.length);

    System.out.println("----------------------------------- ResourcePatternResolver for fileSystem resource -----------------------------------");
    // 也就是说,之前在 ResourceLoader.getResource方法中使用的路径,加上通配符就能在 ResourcePatternResolver.getResources 中使用,相当方便
    Resource[] fileResources = resourcePatternResolver.getResources("file:./doc/*");
    System.out.println("classpath* plus ant-style path: "+fileResources.length);
    for (Resource fileResource : fileResources) {
        boolean isFile = fileResource.isFile();
        if (isFile) {
            File file = fileResource.getFile();
            System.out.println(file.getPath());
        }
    }
}
总结

实际上我们使用 PathMatchingResourcePatternResolver 也就四种情况

ApplicationContext - 重点

ApplicationContext 继承了 ResourcePatternResolver 接口,可直接使用 getResource 和 getResources 方法,非常方便。

Spring Bean 可以通过实现 ResourceLoaderAware 获取一个全局的 ResourceLoader(实际上就是 ApplicationContext 自身),然后可以非常方便地查询资源。非常的简单。

实践
public static void main7(String[] args) throws IOException {
    // 直接用 ConfigurableApplicationContext 效果是一样的。方便了,终于知道为什么要在 ApplicationContext 上实现那么多东西了,都是为了方便
    ConfigurableApplicationContext context = SpringApplication.run(ResourceApplication.class, args);
    Resource resource = context.getResource("classpath:META-INF/spring.factories");

    Resource[] resources0 = context.getResources("classpath:META-INF/spring.factories");
    System.out.println("start with classpath:"+resources0.length);
    Resource[] resources1 = context.getResources("classpath*:META-INF/spring.factories");
    System.out.println("start with classpath*:"+resources1.length);

    //
    //context.getResource();
    Resource[] resources = context.getResources("classpath*:META-INF/*.factories");
    System.out.println("classpath* plus ant-style path: "+resources.length);
    // 查看类路径下指定包下的所有class类,比如我的
    Resource[] resourcesAll = context.getResources("classpath*:xyz/xiashuo/**/*.class");
    System.out.println("classpath* plus ant-style path: "+resourcesAll.length);
    //context.getResources()
    Resource[] fileResources = context.getResources("file:./doc/*");
    System.out.println("classpath* plus ant-style path: "+fileResources.length);
    for (Resource fileResource : fileResources) {
        boolean isFile = fileResource.isFile();
        if (isFile) {
            File file = fileResource.getFile();
            System.out.println(file.getPath());
        }
    }
}

输出

start with classpath:1
start with classpath*:3
classpath* plus ant-style path: 3
classpath* plus ant-style path: 1
classpath* plus ant-style path: 3
E:\IDEAProject\Spring5Framework\Resource\.\doc\aaa.txt
E:\IDEAProject\Spring5Framework\Resource\.\doc\bbb.txt
E:\IDEAProject\Spring5Framework\Resource\.\doc\ccc.txt

总结

最开始的时候,我们要获取资源,要么手动通过创建 File 实例来获取资源,要么只能通过 Class 实例或者 ClassLoader 的 getResource 或 getResources 方法,如果是互联网资源,就得手动创建 URL 实例。

参考《Java 中的常见路径和资源获取.md》

然后 Spring 开始从中抽象出一个顶级接口 Resource,同时将我们之前的各种资源都包装成 Resource 的一个实现类,包括 File、ClassPath、URL 等,这样资源的管理就开始变得统一起来,然后,Spring 还提供了 ResourceLoader 资源加载器,有了资源加载器,程序员可以完全不用关心底层返回的是哪一种类型的 Resource 的实现,只要传入正确的资源路径,即可获取 Resource 资源,极大的简化了资源的获取操作,而且为了一次性获取多个资源,ResourcePatternResolver 还在 ResourceLoader 的基础上进行了拓展,让其可以通过在资源位置中使用 classpath*: 前缀搭配通配符一次性获取多个资源,而随处可见的应用上下文 ApplicationContext 就实现了 ResourcePatternResolver,因此,通过 ApplicationContext 的 getResource 和 getResources 方法,我们就能非常方便地获取资源了。