Spring IoC资源管理之Resource

hresh 617 0

Spring IoC资源管理之Resource

概述

在学 Java SE 的时候我们学习了一个标准类 java.net.URL,该类在 Java SE 中的定位为统一资源定位器(Uniform Resource Locator),但是我们知道它的实现基本只限于网络形式发布的资源的查找和定位。然而,实际上资源的定义比较广泛,除了网络形式的资源,还有二进制形式、文件形式、 字节流形式存在的等等。而且它们可以存在于任何场所,比如网络、文件系统、应用程序中。所以 java.net.URL 的局限性迫使 Spring 必须实现自己的资源管理策略,资源管理应该满足如下要求:

  1. 职能划分清楚。资源的定义和资源的加载应该要有一个清晰的界限;
  2. 统一的抽象。统一的资源定义和资源加载策略。资源加载后要返回统一的抽象给客户端,客户端要对资源进行怎么的处理,应该由抽象资源接口来界定。

Resource

org.springframework.core.io.Resource 为 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource接口。作为所有资源的统一抽象,Resource 定义了一些通用的方法,由子类 AbstractResource 提供统一的默认实现。定义如下:

public interface Resource extends InputStreamSource {
    boolean exists();

    default boolean isReadable() {
        return this.exists();
    }

    default boolean isOpen() {
        return false;
    }

    default boolean isFile() {
        return false;
    }

    URL getURL() throws IOException;

    URI getURI() throws IOException;

    File getFile() throws IOException;

    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(this.getInputStream());
    }

    long contentLength() throws IOException;

    long lastModified() throws IOException;

    Resource createRelative(String var1) throws IOException;

    @Nullable
    String getFilename();

    String getDescription();
}

Resource 结构图如下:

Spring IoC资源管理之Resource

从上图可以看到,Resource 根据资源的不同类型提供不同的具体实现,如下:

  • FileSystemResource :对 java.io.File 类型的资源的封装,只要是和 File 有关联,基本上与 FileSystemResource 也有关联。支持解析为 File 和 URL,实现扩展 WritableResource 接口。注意:从 Spring Framework 5.0开始,FileSystemResource 实现使用 NIO.2 API 进行读/写交互。从5.1开始,它可以使用一个 Path 句柄构造,在这种情况下, 它将通过NIO.2执行所有文件系统交互,仅File依靠on getFile()
  • ByteArrayResource:对字节数组提供的数据的封装。如果通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream。
  • UrlResource:对 java.net.URL 类型资源的封装。内部委派 URL 进行具体的资源操作。
  • ClassPathResource :class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
  • InputStreamResource :将给定的 InputStream 作为一种资源的 Resource 的实现类。
  • PathResource:ResourcePath 句柄的实现,通过 PathAPI 执行所有操作和转换。支持解析为 File 和 URL。实现扩展 WritableResource 接口。注意:从5.1版本开始,Path 也支持 FileSystemResource,因此不再推荐使用 PathResource,赞成使用 FileSystemResource.FileSystemResource(Path)
  • ServletContextResource : 为访问 Web 容器上下文中的资源而设计的类,负责以相对于 Web 应用根目录的路径加载资源,它支持以流和 URL 的方式访问,在 war 解包的情况下,也可以通过 File 的方式访问,该类还可以直接从 jar 包中访问资源。针对于 ServletContext 封装的资源,用于访问 ServletContext 环境下的资源。 ServletContextResource 持有一个 ServletContext 的引用 ,其底层是通过 ServletContext 的 getResource()方法和 getResourceAsStream()方法来获取资源的。

假设现在有一个资源在 Web 应用的类路径下,我们可以有几种方式来访问这个资源呢?

  1. 通过 FileSystemResource 以文件系统绝对路径的方式进行访问(如: D:/spring/WebRoot/WEB-INF/classes/conf.xml)
  2. 通过 ClassPathResource 以类路径的方式进行访问(如:conf.xml)
  3. 通过 ServletContextResource 以相对于 Web 应用根目录的方式进行访问(如: ServletContextResource(ServletContext,"/WEB-INF/classes/conf.xml”)) ,可以通过 ContextLoader.getCurrentWebApplicationContext().getServletContext()来获取 ServletContext。

AbstractResource

AbstractResource 为 Resource 接口的默认实现,它实现了 Resource 接口的大部分的公共实现,作为 Resource 接口中的重中之重,其定义如下:

public abstract class AbstractResource implements Resource {
    public AbstractResource() {
    }

    //如果是文件类型,则判断是否存在,后则关闭对应的流
    public boolean exists() {
        Log logger;
        if (this.isFile()) {
            try {
                return this.getFile().exists();
            } catch (IOException var4) {
                logger = LogFactory.getLog(this.getClass());
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not retrieve File for existence check of " + this.getDescription(), var4);
                }
            }
        }

        try {
            this.getInputStream().close();
            return true;
        } catch (Throwable var3) {
            logger = LogFactory.getLog(this.getClass());
            if (logger.isDebugEnabled()) {
                logger.debug("Could not retrieve InputStream for existence check of " + this.getDescription(), var3);
            }

            return false;
        }
    }

    //直接返回true,表示可读
    public boolean isReadable() {
        return this.exists();
    }

    //直接返回false,表示未打开
    public boolean isOpen() {
        return false;
    }

    //直接返回false,表示不是File
    public boolean isFile() {
        return false;
    }

    //抛出 FileNotFoundException 异常,交给子类实现
    public URL getURL() throws IOException {
        throw new FileNotFoundException(this.getDescription() + " cannot be resolved to URL");
    }

    //基于 getURL() 返回的 URL 构建 URI
    public URI getURI() throws IOException {
        URL url = this.getURL();

        try {
            return ResourceUtils.toURI(url);
        } catch (URISyntaxException var3) {
            throw new NestedIOException("Invalid URI [" + url + "]", var3);
        }
    }

    //抛出 FileNotFoundException 异常,交给子类实现
    public File getFile() throws IOException {
        throw new FileNotFoundException(this.getDescription() + " cannot be resolved to absolute file path");
    }

    //根据 getInputStream() 的返回结果构建 ReadableByteChannel
    public ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(this.getInputStream());
    }

    //获取资源的长度
    public long contentLength() throws IOException {
        InputStream is = this.getInputStream();
        boolean var16 = false;

        long var6;
        try {
            var16 = true;
            long size = 0L;
            byte[] buf = new byte[256];

            while(true) {
                int read;
                if ((read = is.read(buf)) == -1) {
                    var6 = size;
                    var16 = false;
                    break;
                }

                size += (long)read;
            }
        } finally {
            if (var16) {
                try {
                    is.close();
                } catch (IOException var17) {
                    Log logger = LogFactory.getLog(this.getClass());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Could not close content-length InputStream for " + this.getDescription(), var17);
                    }
                }

            }
        }

        try {
            is.close();
        } catch (IOException var18) {
            Log logger = LogFactory.getLog(this.getClass());
            if (logger.isDebugEnabled()) {
                logger.debug("Could not close content-length InputStream for " + this.getDescription(), var18);
            }
        }

        return var6;
    }

    //返回资源最后的修改时间
    public long lastModified() throws IOException {
        File fileToCheck = this.getFileForLastModifiedCheck();
        long lastModified = fileToCheck.lastModified();
        if (lastModified == 0L && !fileToCheck.exists()) {
            throw new FileNotFoundException(this.getDescription() + " cannot be resolved in the file system for checking its last-modified timestamp");
        } else {
            return lastModified;
        }
    }

    protected File getFileForLastModifiedCheck() throws IOException {
        return this.getFile();
    }

    public Resource createRelative(String relativePath) throws IOException {
        throw new FileNotFoundException("Cannot create a relative resource for " + this.getDescription());
    }

    @Nullable
    public String getFilename() {
        return null;
    }

    public boolean equals(@Nullable Object other) {
        return this == other || other instanceof Resource && ((Resource)other).getDescription().equals(this.getDescription());
    }

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

    public String toString() {
        return this.getDescription();
    }
}

如果我们想要实现自定义的 Resource,记住不要实现 Resource 接口,而应该继承 AbstractResource 抽象类,然后根据当前的具体资源特性覆盖相应的方法即可。

ClassPathResource

一个应用上下文构造器一般需要一个构成 Bean 定义的 xml 文件字符串路径或者一个字符串数组路径作为参数。

当这样的路径没有前缀的时候,那么从哪个路径构建的资源类型,用于加载 bean 的定义,取决于它所处的指定的上下文环境,例如,如果你像下面一样创建一个ClassPathXMLApplicationContext :

ApplicationContext context = new ClassPathXmlApplicationContext("application_context.xml");
//或者下面这个方式
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application_context.xml");

Bean 定义将会从 classpath 中加载然后形成一个 ClassPathResource 来使用。基于此种情况,我们对 ClassPathResource 学习了解一下。

ClassPathResource 类是对 classpath 下资源的封装,或者是说对 ClassLoader.getResource()方法或 Class.getResource()方法的封装 ,它支持在当前 classpath 中读取资源文件。可以传入相对 classpath 的文件全路径名和 ClassLoader 构建 ClassPathResource,或忽略 ClassLoader 采用默认ClassLoader(即DefaultResourceLoader),此时在 getInputStream()方法 实现时会使用 ClassLoader.getSystemResourceAsStream(path)方法。 由于使用 ClassLoader 获取资源时默认相对于 classpath 的根目录,因而构造函数会忽略开头的“/”字符。ClassPathResource 还可以使用文件路径和 Class 作为参数构建,此时文件路径需要以“/”开头,表示该文件为相对于classpath 的绝对路径,否则为相对 Class 实例的相对路径,然后程序会报错,在 getInputStream()方法实现时使用 Class.getResourceAsStream()方法。

先看一个案例:

@Test
public void getResource() throws IOException {
    //ClassPathResource
    ClassPathResource resource = new ClassPathResource("application_context.xml");
    //        ClassPathResource resource = new ClassPathResource("/application_context.xml", User.class);
    InputStream input = resource.getInputStream();
    Assert.assertNotNull(input);
    System.out.println(resource.getClassLoader());
    System.out.println(resource.getPath());

}

上述两种构建方式都是正确的,分别对应 ClassPathResource 的两个构造方法,源码如下:

public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
    Assert.notNull(path, "Path must not be null");
    String pathToUse = StringUtils.cleanPath(path);
    if (pathToUse.startsWith("/")) {
        pathToUse = pathToUse.substring(1);
    }

    this.path = pathToUse;
    this.classLoader = classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader();
}

public ClassPathResource(String path, @Nullable Class<?> clazz) {
    Assert.notNull(path, "Path must not be null");
    this.path = StringUtils.cleanPath(path);
    this.clazz = clazz;
}

接着再来查看 getInputStream()方法源码,

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(this.getDescription() + " cannot be opened because it does not exist");
    } else {
        return is;
    }
}

关于获取资源的方式有两种:Class 获取和 ClassLoader 获取。

package ioc_study;
public class ResourceTest {

    @Test
    public void getResource() {

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        System.out.println(classLoader.getResource("").getPath());

        System.out.println(this.getClass().getResource("").getPath());
        System.out.println(this.getClass().getResource("/").getPath());

        System.out.println(System.getProperty("user.dir"));
    }
}

执行结果为:

/F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/test-classes/
/F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/test-classes/ioc_study/
/F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/test-classes/
F:\workspace\Spmvc_Learn\spring_study\spring-chap1
  • ClassLoader.getResource("")获取的是 classpath 的根路径
  • Class.getResource("")获取的是相对于当前类的相对路径
  • Class.getResource("/")获取的是 classpath 的根路径
  • System.getProperty("user.dir")获取的是项目的路径

相关文件的路径图如下:

Spring IoC资源管理之Resource

关于 Class 和 ClassLoader 访问资源的区别,可以参考这篇文章:关于Class.getResource和ClassLoader.getResource的路径问题

在创建 ClassPathResource 对象时,我们可以指定是按 Class 的相对路径获取文件还是按 ClassLoader 来获取。

FileSystemResource

构建应用上下文除了使用 ClassPathXMLApplicationContext 之外,还可以使用 FileSystemXxmlApplicationContext ,代码如下:

ApplicationContext context = new ClassPathXmlApplicationContext("target/classes/application_context.xml");
//或者下面这个方式
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:target/classes/application_context.xml");

FileSystemResourcey 会绑定到 FileSystemXxmlApplicationContext ,所以我们接下来学习一下 FileSystemResourcey 。

FileSystemResourcey 是对 File 的封装,在构建 FileSystemResourcey 时可以传入 File 对象或路径字符串(这里的路径可以是相对路径,相对路径是相对于 System.getProperty(“user.dir”) 的值所在的路径,也可以是绝对路径,也可以是“file:”开头的路径值),在内部会创建相应的 File 对象,并且计算其 path 值,这里的 path 是计算完“.”和“..”影响的值(规格化)。

看下面这个案例:

@Test
public void getResource() throws IOException {
   //        FileSystemResource resource1 = new FileSystemResource("target/classes/application_context.xml");
        FileSystemResource resource1 = new FileSystemResource("F:\\workspace\\Spmvc_Learn\\spring_study\\spring-chap1\\target\\classes\\application_context.xml");
    InputStream input = resource1.getInputStream();
    Assert.assertNotNull(input);
    System.out.println(resource1.getPath());

}

接着查看其构造方法代码,

public FileSystemResource(String path) {
    Assert.notNull(path, "Path must not be null");
    this.path = StringUtils.cleanPath(path);
    this.file = new File(path);
    this.filePath = this.file.toPath();
}

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

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

在 getInputStream 方法中,使用该 File 对象创建 FileInputStream, 代码实现比较简单。

public InputStream getInputStream() throws IOException {
    try {
        return Files.newInputStream(this.filePath);
    } catch (NoSuchFileException var2) {
        throw new FileNotFoundException(var2.getMessage());
    }
}

另外再介绍一下 createRelative 方法,源码如下:

public Resource createRelative(String relativePath) {
    String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
    return this.file != null ? new FileSystemResource(pathToUse) : new FileSystemResource(this.filePath.getFileSystem(), pathToUse);
}

createRelative 方法中使用 path 计算相对路径,其算法是:找到最后一个路径分隔符(/),将相对路径添加到该分隔符之后,传入的相对路径可以是以路径分割符(/)开头,也可以不以分隔符(/)开头,它们的效果是一样的,对相对路径存在的“.”和“..”会在创建FileSystemResource类时处理。最后,当使用将一个目录的 File 对象构建FileSystemResource 时,调用 createRelative 方法,其相对路径的父目录和当前 FileSystemResource 的父目录相同,比如使用”target/classes/application_context.xml”路径创建 FileSystemResource 对象,该 Resource 对象调用 createRelative,并传入”application_context.xml”,那么出现的结果为绝对路径。

@Test
public void getResource() throws IOException {
    System.out.println(System.getProperty("user.dir"));

    //FileSystemResource
    FileSystemResource resource1 = new FileSystemResource("target/classes/application_context.xml");//相对路径
    //FileSystemResource resource1 = new FileSystemResource("F:\\workspace\\Spmvc_Learn\\spring_study\\spring-chap1\\target\\classes\\application_context.xml");//绝对路径
    InputStream input = resource1.getInputStream();
    Assert.assertNotNull(input);

    System.out.println(resource1.getPath());
    System.out.println(resource1.getDescription());

    Resource resource = resource1.createRelative("application_context.xml");
    System.out.println(resource);
}

执行结果为:

F:\workspace\Spmvc_Learn\spring_study\spring-chap1
target/classes/application_context.xml
file [F:\workspace\Spmvc_Learn\spring_study\spring-chap1\target\classes\application_context.xml]
file [F:\workspace\Spmvc_Learn\spring_study\spring-chap1\target\classes\application_context.xml]

扩展:关于 FileSystemResource 的构建,如果不清楚文件的相对路径,还可以用以下方式进行实现。

@Test
public void getResource() throws IOException {
    URL url = getClass().getResource("application_context.xml");
    System.out.println("url.toURI():"+url.toURI());
    System.out.println("url.getPath():"+url.getPath());

    Path xmlPath = new File(url.getPath()).toPath();
    System.out.println("xmlPath:"+xmlPath);
    //这里传入的参数都是绝对路径
    FileSystemResource resource1 = new FileSystemResource(url.getPath());
    //   FileSystemResource resource1 = new FileSystemResource(xmlPath);
    System.out.println(resource1.getPath());
    System.out.println(resource1.getDescription());
}

执行结果为:

url.toURI():file:/F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/classes/application_context.xml
url.getPath():/F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/classes/application_context.xml
xmlPath:F:\workspace\Spmvc_Learn\spring_study\spring-chap1\target\classes\application_context.xml
/F:/workspace/Spmvc_Learn/spring_study/spring-chap1/target/classes/application_context.xml
file [F:\workspace\Spmvc_Learn\spring_study\spring-chap1\target\classes\application_context.xml]

关于 Spring 构建应用上下文涉及到的两种 Resource简单介绍了一下,如果有兴趣的朋友对于 Resource 想多了解一些,可以阅读: 深入Spring IOC源码之Resource


参考文献

Spring学习之资源管理器(Resource)

深入Spring IOC源码之Resource

【死磕 Spring】—– IOC 之 Spring 统一资源加载策略

Spring之ClassPathResource加载资源文件

Spring Resource之应用上下文和资源路径

Springboot文件下载

发表评论 取消回复
表情 图片 链接 代码

分享