Spring(七):资源操作 Resources

Java 的标准 java.net.URL 类和各种 URL 前缀的标准处理程序无法满足所有对 low-level 资源的访问,比如:没有标准化的 URL 实现可用于访问需要从类路径或相对于 ServletContext 获取的资源。并且缺少某些 Spring 所需要的功能,例如检测某资源是否存在等。而 Spring 的 Resource 声明了访问 low-level 资源的能力。

Resource 接口

Spring 的 Resource 接口位于 org.springframework.core.io 中。 是一个强大的接口,用于抽象对低级资源的访问。以下显示了 Resource 接口定义的方法:

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isReadable();

    boolean isOpen();

    boolean isFile();

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    ReadableByteChannel readableChannel() throws IOException;

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();
}

其中一些重要的方法:

  • getInputStream(): 找到并打开资源,返回一个 InputStream 以从资源中读取。预计每次调用都会返回一个新的I nputStream(),调用者有责任关闭每个流。
  • exists(): 返回一个布尔值,表明某个资源是否以物理形式存在
  • isOpen(): 返回一个布尔值,指示此资源是否具有开放流的句柄。如果为 true,InputStream 就不能够多次读取,只能够读取一次并且及时关闭以避免内存泄漏。对于所有常规资源实现,返回 false,但是 InputStreamResource 除外。
  • getDescription(): 返回资源的描述,用来输出错误的日志。这通常是完全限定的文件名或资源的实际 URL。

其他方法:

  • isReadable(): 表明资源的目录读取是否通过 getInputStream() 进行读取。
  • isFile(): 表明这个资源是否代表了一个文件系统的文件。
  • getURL(): 返回一个 URL 句柄,如果资源不能够被解析为 URL,将抛出 IOException
  • getURI(): 返回一个资源的 URI 句柄
  • getFile(): 返回某个文件,如果资源不能够被解析称为绝对路径,将会抛出 FileNotFoundException
  • lastModified(): 资源最后一次修改的时间戳
  • createRelative(): 创建此资源的相关资源
  • getFilename(): 资源的文件名是什么 例如:最后一部分的文件名 myfile.txt

Resource 实现类

Resource 接口是 Spring 资源访问策略的抽象,是基于策略模式的。它本身并不提供任何资源访问实现,具体的资源访问由该接口的实现类完成——每个实现类代表一种资源访问策略。Resource 一般包括这些实现类:UrlResource、ClassPathResource、FileSystemResource、ServletContextResource、InputStreamResource、ByteArrayResource

image-20230401115229011

UrlResource(常用)

Resource 的一个实现类,常用来访问网络资源,它支持 URL 的绝对路径。它也支持访问系统资源,功能最强大。

  • http:该前缀用于访问基于 HTTP 协议的网络资源。
  • ftp:该前缀用于访问基于 FTP 协议的网络资源
  • file:该前缀用于从文件系统中读取资源
Resource resource;
@Test
public void urlResourceTest() throws Exception {
    // http
    // String path = "http://www.baidu.com";

    // file,注意指向的是工程的根路径,而不是模块的根路径
    // 当然也可以写绝对路径:file:D:/Documents/Links.txt
    String path = "file:pom.xml";

    // ftp 协议也可以通过这种方式访问

    // 1. 创建 UrlResource 实现类
    resource = new UrlResource(path);

    // 2. 获取 UrlResource 实现类的信息
    System.out.println(resource.getFilename());
    System.out.println(resource.getURI());
    System.out.println(resource.getDescription());
    System.out.println(resource.getInputStream().read());
}

ClassPathResource(常用)

ClassPathResource 用来访问类加载路径下的资源,相对于其他的 Resource 实现类,其主要优势是方便访问类加载路径里的资源,尤其对于 Web 应用,ClassPathResource 可自动搜索位于 classes 下的资源文件,无须使用绝对路径访问。

    @Test
    public void classPathResourceTest() throws Exception {
        // ClassPathResource 的好处是直接默认处于当前模块的根目录,
        // 也就是说对于当前模块 resources 下的文件可以直接访问
        String path = "penghao.txt";

        // 1. 创建 ClassPathResource 实现类
        resource = new ClassPathResource(path);

        // 2. 获取 ClassPathResource 实现类的信息
        System.out.println(resource.getFilename());
        System.out.println(resource.getURI());
        System.out.println(resource.getDescription());
        InputStream inputStream = resource.getInputStream();
        byte[] b = new byte[1024];
        int len;
        while ((len = inputStream.read(b)) != -1) {
            System.out.print(new String(b, 0, len));
        }
    }

FileSystemResource(常用)

Spring 提供的 FileSystemResource 类用于访问文件系统资源

使用 FileSystemResource 来访问文件系统资源并没有太大的优势,因为 Java 提供的 File 类也可用于访问文件系统资源。

@Test
public void fileSystemResourceTest() throws Exception {
    // 这里可以直接使用绝对路径
    String path = "E:\\Documents\\Links.txt";

    // 相对路径是工程的根路径,和 UrlResource 类似

    // 1. 创建 FileSystemResource 实现类
    resource = new FileSystemResource(path);

    // 2. 获取 FileSystemResource 实现类的信息
    System.out.println(resource.getFilename());
    System.out.println(resource.getURI());
    System.out.println(resource.getDescription());
    InputStream inputStream = resource.getInputStream();
    byte[] b = new byte[1024];
    int len;
    while ((len = inputStream.read(b)) != -1) {
        System.out.print(new String(b, 0, len));
    }
}

ServletContextResource

这是 ServletContext 资源的 Resource 实现,它解释相关 Web 应用程序根目录中的相对路径。它始终支持流(stream)访问和 URL 访问,但只有在扩展 Web 应用程序存档且资源实际位于文件系统上时才允许 java.io.File 访问。无论它是在文件系统上扩展还是直接从 JAR 或其他地方(如数据库)访问,实际上都依赖于 Servlet 容器。

InputStreamResource

InputStreamResource 是给定的输入流(InputStream)的 Resource 实现。它的使用场景在没有特定的资源实现的时候使用(感觉和@Component 的适用场景很相似)。与其他 Resource 实现相比,这是已打开资源的描述符。 因此,它的 isOpen() 方法返回true。如果需要将资源描述符保留在某处或者需要多次读取流,请不要使用它。

ByteArrayResource

字节数组的 Resource 实现类。通过给定的数组创建了一个 ByteArrayInputStream。它对于从任何给定的字节数组加载内容非常有用,而无需求助于单次使用的 InputStreamResource。

ResourceLoader 接口

ResourceLoader 接口实现类的实例可以获得一个 Resource 实例。在ResourceLoader接口里有且仅有一个方法:

  • Resource getResource(String location) : 用于返回一个 Resource 实例。

ApplicationContext 的所有实现类都实现了 ResourceLoader 接口,这意味着它们都可以使用 getResource() 获得 Resource 实例。获得的 Resource 的具体类型与 ApplicationContext 的类型有关。

例如:

  • 使用 ClassPathXmlApplicationContext

    @Test
    public void test() {
        ApplicationContext context = new ClassPathXmlApplicationContext();
        Resource res = context.getResource("penghao.txt");
        System.out.println(res.getDescription());
    }
    

    得到的是当前模板下类加载路径的文件,也即 IDEA 中当前模块 resources 中的文件。

  • 使用 FileSystemXmlApplicationContext

    @Test
    public void test2() {
        ApplicationContext context = new FileSystemXmlApplicationContext();
        Resource res = context.getResource("pom.xml");
        System.out.println(res.getDescription());
    }
    

    得到的是整个工程根目录下的文件。

需要使用 Resource 时,既可以使用 ApplicationContext 获得,也可以自行创建不同类型的 Resource 实现类以实现各种资源的访问。

ResourceLoaderAware 接口

ResourceLoaderAware 接口实现类的实例拥有一个 ResourceLoader 的引用,该接口有且仅有一个方法:

  • void setResourceLoader(ResourceLoader resourceLoader) : 用于设定拥有的 ResourceLoader 实例。

如果把ResourceLoaderAware 接口的实现类部署在 bean 容器中,Spring 容器会将自身作为 setResourceLoader() 方法的参数传入。由于 ApplicationContext 的实现类都实现了 ResourceLoader 接口,所以都可以作为参数传入 setResourceLoader。

例如我们创建一个简单的 ResourceLoaderAware 接口实现类来验证:

package site.penghao.spring6.resource_loader_aware;

import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;

/**
 * @author hope
 * @date 2023/4/1 - 12:25
 */
@Component
public class TestBean implements ResourceLoaderAware {
    private ResourceLoader resourceLoader;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }
}

测试结果:

/**
 * @author hope
 * @date 2023/4/1 - 12:28
 */
public class ResourceLoaderAwareTest {

    @Test
    public void test() {
        ApplicationContext context =
                new ClassPathXmlApplicationContext("bean.xml");
        TestBean testBean = context.getBean(TestBean.class);

        System.out.println(testBean.getResourceLoader() == context); // true
    }
}

依赖注入 Resource

使用前面的方法,如果需要获取 Resource 实例,都需要在代码中输入 path 路径,如果需要修改路径,就需要修改代码,这是我们不希望的。Spring 实际上有更好的解决方案:使用依赖注入。Spring 框架不仅充分地利用了策略模式来简化资源访问,还将策略模式和 IoC 进行充分地结合,最大程度地简化了 Spring 资源访问。

具体地,以依赖注入的方式获取资源的基本步骤如下:

  1. 环境准备:待注入的 Resource 属性

    package site.penghao.spring6.resource;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.io.Resource;
    import org.springframework.stereotype.Component;
    
    /**
     * @author hope
     * @date 2023/4/1 - 12:46
     */
    public class ResourceBean {
        private Resource resource;
    
        public Resource getResource() {
            return resource;
        }
    
        public void setResource(Resource resource) {
            this.resource = resource;
        }
    }
    
  2. 配置 bean,注入 Resource 属性(这是一种特殊的注入:使用 value 而不是 ref)

    <bean id="resourceBean" class="site.penghao.spring6.resource.ResourceBean">
        <!-- 可以使用file:、http:、ftp:等前缀强制Spring采用对应的资源访问策略 -->
        <!-- 如果不采用任何前缀,则Spring将采用与该ApplicationContext相同的资源访问策略来访问资源 -->
        <property name="resource" value="classpath:penghao.txt"/>
    </bean>
    
  3. 测试

    @Test
    public void test() {
        ApplicationContext context =
                new ClassPathXmlApplicationContext("bean.xml");
        ResourceBean resourceBean = context.getBean(ResourceBean.class);
        Resource resource = resourceBean.getResource();
        System.out.println(resource.getDescription()); // class path resource [penghao.txt]
    }
    

配置 ApplicationContext 的资源访问策略

不管以怎样的方式创建 ApplicationContext 实例,都需要为 ApplicationContext 指定一份或多份配置文件。然而,根路径的含义却并不相同,例如使用 ClassPathXMLApplicationContext 指定配置文件时,根路径是类加载路径;使用 FileSystemXmlApplicationContext 指定配置文件时,根路径是整个工程的根路径。事实上,创建 ApplicationContext 时的资源访问策略就是基于不同的 Resource 策略进行的。

ApplicationContext 确定资源访问策略通常有两种方法:

  • 使用 ApplicationContext 实现类指定访问策略。
  • 使用前缀指定访问策略。

使用 ApplicationContext 实现类指定访问策略

就是我们以前一直使用的方式,例如可以使用下面的实现类进行配置文件的设置:

  • ClassPathXMLApplicationContext :对应使用 ClassPathResource 进行资源访问。
  • FileSystemXmlApplicationContext :对应使用 FileSystemResource 进行资源访问。
  • XmlWebApplicationContext :对应使用 ServletContextResource 进行资源访问。

当使用 ApplicationContext 的不同实现类时,就意味着 Spring 使用相应的资源访问策略。

使用前缀指定访问策略

classpath 前缀使用

通过指定classpath:前缀强制搜索类加载路径

public class Demo1 {

    public static void main(String[] args) {
        /*
         * 通过搜索文件系统路径下的xml文件创建ApplicationContext,
         * 但通过指定classpath:前缀强制搜索类加载路径
         * classpath:bean.xml
         * */
        ApplicationContext ctx =
                new ClassPathXmlApplicationContext("classpath:bean.xml");
        System.out.println(ctx);
        Resource resource = ctx.getResource("penghao.txt");
        System.out.println(resource.getFilename());
        System.out.println(resource.getDescription());
    }
}

classpath 通配符使用

classpath * :前缀提供了加载多个 XML 配置文件的能力,当使用 classpath*: 前缀来指定XML配置文件时,系统将搜索类加载路径,找到所有与文件名匹配的文件,分别加载文件中的配置定义,最后合并成一个 ApplicationContext。

ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:bean.xml");
System.out.println(ctx);

当使用classpath * :前缀时,Spring将会搜索类加载路径下所有满足该规则的配置文件。

如果不是采用classpath *: 前缀,而是改为使用classpath:前缀,Spring 则只加载第一个符合条件的 XML 文件

注意 :

classpath * :前缀仅对 ApplicationContext 有效。实际情况是,创建 ApplicationContext 时,分别访问多个配置文件(通过 ClassLoader 的 getResources 方法实现)。因此,classpath * : 前缀不可用于 Resource。

通配符其他使用

一次性加载多个配置文件的方式:指定配置文件时使用通配符

ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:bean*.xml");

Spring允许将classpath*:前缀和通配符结合使用:

ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:bean*.xml");

小结

  • Resource 是 Spring 提供的用于访问各种资源的接口,它抽象了对各种低级资源的访问方法。这种实现是基于策略模式的,Resource 的实现类提供了不同的访问的策略以方便调用。
  • 常用的 Resource 实现类有 UrlResource、ClassPathResource、FileSystemResource、ServletContextResource、InputStreamResource、ByteArrayResource 等,它们代表不同的资源访问策略,可以使用不同的参数以访问各种资源。
  • ResourceLoader 接口包含的 getResource() 方法可以获得 Resource 策略,所有的 ApplicationContext 都实现了 ResourceLoader 接口。
  • ResourceLoaderAware 接口包含的 setResourceLoader() 方法需要设置一个 ResourceLoader,如果一个类实现了 ResourceLoaderAware 接口并且被配置为 bean,那么 IoC 容器会将自身作为 setResourceLoader() 的参数传递给这个实现类。
  • 通过依赖注入的方式注入 Resource 属性的好处是可以将配置路径和程序代码分离,注入时,只需将属性的 value 值设置为路径即可。
  • 确定 ApplicationContext 的配置资源访问策略既可以使用不同的实现类区分,也可以使用 classpath: 前缀强制使用类加载路径。