1、概述
java自带的java.net.URL,虽然定位为统一资源定位器(Uniform Resource Locator),但是我们知道它基本只实现了只限于网络形式发布的资源的查找和定位。但是,实际上,资源的定义是相当广泛的,除了网络形式的资源,还有二进制、文件形式、字节流形式的资源的,而且他们存在的地方也是多种多样的,例如网络、应用程序、文件系统中。所以java.net.URL
的局限性是相当大的,因此Spring就必须实现自己的资源管理策略。应满足以下的要求:
- 职能划分清楚,资源的定义和资源的加载必须分开。
- 统一的抽象。统一的资源定义和资源加载策略。资源加载后要返回统一的抽象给客户端,客户端要对资源进行怎么样的处理,应该由抽象的资源接口来界定。
2、Resource
org.springframework.core.io.Resource
为Spring框架所有资源的抽象和访问接口,它继承了org.springframework.core.io.Resource
接口,作为所有资源的统一抽象,Resource接口实现了一些通用的方法,由它的子类提供默认的统一实现。Resource的定义如下:
public interface Resource extends InputStreamSource {
boolean exists();
default boolean isReadable() {
return 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(getInputStream());
}
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
@Nullable
String getFilename();
String getDescription();
}
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
依靠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()
方法来获取资源的。
思考
假设现在有个资源demo1.xml在Web应用的类路径下,我们可以通过哪几种方式来访问这个资源呢?
- 通过FileSystemResource以文件系统的绝对路径进行访问(如:D:/spring/WebRoot/WEB-INF/classes/conf.xml)
- 通过ClassPathResource以类路径进行访问(如:conf.xml)
- 通过ServletContextResource以相对于web应用根目录的方式进行访问(如:先通过
ContextLoader.getCurrentWebApplicationContext().getServletContext()
获取ServletContext,然后通过ContextLoader.getCurrentWebApplicationContext().getServletContext()
获取相关的文件信息)
3、AbstractResource
AbstractResource作为Resource的默认实现类,它实现了Resource的大部分方法,是Resource接口中的重中之重,定义如下:
public abstract class AbstractResource implements Resource {
@Override
public boolean exists() {
// 如果是文件类型
if (isFile()) {
try {
return getFile().exists();
}
catch (IOException ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Could not retrieve File for existence check of " + getDescription(), ex);
}
}
}
// 不是文件类型
try {
getInputStream().close();
return true;
}
catch (Throwable ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Could not retrieve InputStream for existence check of " + getDescription(), ex);
}
return false;
}
}
//是否可读
@Override
public boolean isReadable() {
return exists();
}
//是否可以打开
@Override
public boolean isOpen() {
return false;
}
//是否是文件
@Override
public boolean isFile() {
return false;
}
//抛出 FileNotFoundException 异常,交给子类实现
@Override
public URL getURL() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
}
//基于 getURL() 返回的 URL 构建 URI
@Override
public URI getURI() throws IOException {
URL url = getURL();
try {
return ResourceUtils.toURI(url);
}
catch (URISyntaxException ex) {
throw new NestedIOException("Invalid URI [" + url + "]", ex);
}
}
//抛出 FileNotFoundException 异常,交给子类实现
@Override
public File getFile() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
}
//根据 getInputStream() 的返回结果构建 ReadableByteChannel
@Override
public ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}
//获取资源的长度
@Override
public long contentLength() throws IOException {
InputStream is = getInputStream();
try {
long size = 0;
byte[] buf = new byte[256];
int read;
while ((read = is.read(buf)) != -1) {
size += read;
}
return size;
}
finally {
try {
is.close();
}
catch (IOException ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Could not close content-length InputStream for " + getDescription(), ex);
}
}
}
}
//返回资源最后的修改时间
@Override
public long lastModified() throws IOException {
File fileToCheck = getFileForLastModifiedCheck();
long lastModified = fileToCheck.lastModified();
if (lastModified == 0L && !fileToCheck.exists()) {
throw new FileNotFoundException(getDescription() +
" cannot be resolved in the file system for checking its last-modified timestamp");
}
return lastModified;
}
//获取一个适合用于检查最后修改时间的文件
protected File getFileForLastModifiedCheck() throws IOException {
return getFile();
}
@Override
public Resource createRelative(String relativePath) throws IOException {
throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
}
@Override
@Nullable
public String getFilename() {
return null;
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof Resource &&
((Resource) other).getDescription().equals(getDescription())));
}
@Override
public int hashCode() {
return getDescription().hashCode();
}
@Override
public String toString() {
return getDescription();
}
}
如果我们需要实现自定义的Resource接口,切记不要直接实现Resource接口,而是应该去继承AbstractResource抽象类,并且根据需求去重写相关的方法即可。
4、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()
方法。
案例:
public class Demo1 {
@Test
public void getResource() throws IOException {
ClassPathResource resource = new ClassPathResource("demo1.xml");
ClassPathResource resource2 = new ClassPathResource("/demo1.xml", User.class);
InputStream inputStream = resource.getInputStream();
Assert.assertNotNull(inputStream);
System.out.println(resource.getClassLoader());
System.out.println(resource.getPath());
}
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
demo1.xml
构造方法的解析:
public ClassPathResource(String path) {
this(path, (ClassLoader)null);
}
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获取。两种方法的区别如下:
ClassLoader.getResource("")
获取的是 classpath 的根路径Class.getResource("")
获取的是相对于当前类的相对路径Class.getResource("/")
获取的是 classpath 的根路径System.getProperty("user.dir")
获取的是项目的路径
测试代码如下:
public class Demo2 {
@Test
public void test() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<? extends Demo2> aClass = this.getClass();
System.out.println("classLoader.getResource():" + classLoader.getResource(""));
System.out.println("classLoader.getResource(/):" + classLoader.getResource("/"));
System.out.println("aClass.getResource()" + aClass.getResource(""));
System.out.println("aClass.getResource(/)" + aClass.getResource("/"));
System.out.println("System.getProperty(user.dir)" + System.getProperty("user.dir"));
}
}
结果如下:
classLoader.getResource():file:/C:/Users/LinMain/Desktop/spring-demo/demo_resource/target/test-classes/
classLoader.getResource(/):null
aClass.getResource()file:/C:/Users/LinMain/Desktop/spring-demo/demo_resource/target/test-classes/demo2/
aClass.getResource(/)file:/C:/Users/LinMain/Desktop/spring-demo/demo_resource/target/test-classes/
System.getProperty(user.dir)C:\Users\LinMain\Desktop\spring-demo\demo_resource
目录结构如下:
关于 Class 和 ClassLoader 访问资源的区别,可以参考这篇文章:关于Class.getResource和ClassLoader.getResource的路径问题
因此,在创建ClassPathResource对象时,我们可以指定是按照Class的相对路径还是ClassLoader的路径来获取文件。
5、FileSystemResource
构建应用上下文除了使用ClassPathXMLApplicationContext之外,还可以使用FileSystemXmlApplicationContext,代码如下:
ApplicationContext context =
new ClassPathXmlApplicationContext("target/classes/application_context.xml");
//或者下面这个方式
ApplicationContext context =
new ClassPathXmlApplicationContext("classpath:target/classes/application_context.xml");
FileSystemResource会绑定到FileSystemXmlApplicationContext,所以我们接下来学习一下FileSystemResource。
FileSystemResource是对File的封装,在构建FileSystemResource时可以传入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());
}
看看该方法对应的源码(new FileSystemResource()
):
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;
}
在FileSystemResource中的getInputStream()
就很简单了:
public InputStream getInputStream() throws IOException {
try {
return Files.newInputStream(this.filePath);
} catch (NoSuchFileException var2) {
throw new FileNotFoundException(var2.getMessage());
}
}
另外再介绍一下FileSystemResource中的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));
}
private static final String FOLDER_SEPARATOR = "/";
public static String applyRelativePath(String path, String relativePath) {
//获取最后“/”的最后位置
int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR);
if (separatorIndex != -1) {
String newPath = path.substring(0, separatorIndex);
if (!relativePath.startsWith(FOLDER_SEPARATOR)) {
newPath += FOLDER_SEPARATOR;
}
return newPath + relativePath;
}
else {
return relativePath;
}
}
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/demo1.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("demo1.xml");
System.out.println(resource);
}
该代码的执行结果如下:
C:\Users\LinMain\Desktop\spring-demo\demo_resource
target/classes/demo1.xml
file [C:\Users\LinMain\Desktop\spring-demo\demo_resource\target\classes\demo1.xml]
file [C:\Users\LinMain\Desktop\spring-demo\demo_resource\target\classes\demo1.xml]
另外,关于FileSystemResource的构建,如果不清楚文件的相对路径,还可以用以下方式进行实现: