Java中的资源
在Java语言中,使用URL
来表示不同来源的资源,URL
可以根据不同的协议定义不同类型的资源,常见资源类型有(URL支持的协议):
- file:
- http/https:
- jar:
资源通过URL
定义好了之后,如何对不同类型的资源进行解析呢?可以通过注册不同的handler实现,这里的handler指的是java.net.URLStreamHandler
。在URL
的API中有个重要的获取Connection
的方法:URL#openConnection()
,而该方法的实现又委托给URLStreamHandler#openCon nection
。而URLStreamHandler
是个抽象类,恰好其openConnection
方法又是抽象方法,即留给子类自行实现。
在JDK中,URLStreamHandler
的子类如下图所示:
classpath
在日常编程中,我们会常遇到classpath
,但classpath
究竟指的是哪个目录呢?
无论是maven项目还是普通的java工程都会有个src目录,这个目录中存放着项目的java文件和配置文件。web项目src目录下的文件在编译后会放到WEB-INF/classes,非web项目src目录下的文件在编译后会放到classes/。classpath所对应的路径指的就是WEB-INF/classes(或者classes/)
。
classpath 和 classpath* 区别:
classpath:只会到你的class路径中查找找文件;
classpath*:不仅包含class路径,还包括jar文件中(class路径)进行查找.
用classpath*:需要遍历所有的classpath,所以加载速度是很慢的,因此,在规划的时候,应该尽可能规划好资源文件所在的路径,尽量避免使用 classpath*
Java读取资源文件
在讲资源文件的读取之前,我们先来看看绝对路径和相对路径。
在读取某个文件时,我们常会遇到FileNotFoundException
,然而这个文件明明是存在的,导致这个异常的原因往往是由于路径定位得不对。在Java编程中我们可以使用以下两种方式读取资源:
- 使用
java.lang.Class#getResourceAsStream()
; - 使用
java.lang.ClassLoader#getResourceAsStream()
。
使用Class
进行资源加载,支持相对路径和绝对路径。相对路径以当前类所在路径为参考点;绝对路径以"/"开头,classpath为根目录。
使用ClassLoader
进行资源加载,不支持相对路径,都是以classpath为根目录的绝对路径,需要注意的一点是千万不能以"/"开头。
下面看看例子:
public class ResourceReadDemo {
public static void main(String[] args) {
ResourceReadDemo resourceRead = new ResourceReadDemo();
resourceRead.readFromRelativePathByClass();
resourceRead.readFromAbsolutePathByClass();
// classloader加载资源以classpath为相对路径,且不能以"/"开头
resourceRead.readByClassLoader();
}
private void readFromRelativePathByClass(){
try (InputStream is = this.getClass().getResourceAsStream("resourceDemo.properties");) {
Properties prop = new Properties();
prop.load(is);
// output : package
System.out.println(prop.getProperty("key"));
} catch (IOException e) {
e.printStackTrace();
}
}
private void readFromAbsolutePathByClass(){
try (InputStream is = this.getClass().getResourceAsStream("/resourceDemo.properties");) {
Properties prop = new Properties();
prop.load(is);
// output : resources
System.out.println(prop.getProperty("key"));
} catch (IOException e) {
e.printStackTrace();
}
try (InputStream is = this.getClass().getResourceAsStream("/io/zgc/spring/features/resource/resourceDemo.properties");) {
Properties prop = new Properties();
prop.load(is);
// output : package
System.out.println(prop.getProperty("key"));
} catch (IOException e) {
e.printStackTrace();
}
}
private void readByClassLoader() {
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("resourceDemo.properties");) {
Properties prop = new Properties();
prop.load(is);
// output : resources
System.out.println(prop.getProperty("key"));
} catch (IOException e) {
e.printStackTrace();
}
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("io/zgc/spring/features/resource/resourceDemo.properties");) {
Properties prop = new Properties();
prop.load(is);
// output : package
System.out.println(prop.getProperty("key"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
Spring中的资源
Spring对资源单独进行了抽象表示,而没有直接使用URL
,原因如下:
-
URL
没有定义Classpath或ServletContext资源(这点可以通过扩展URL并定义相应的handler来解决); -
URL
没有提供资源是否存在、当前资源是否可读等基本判断方法。
故此,Spring将资源对象抽象成Resource
,Spring会将配置文件都封装成Resource
,它的接口定义如下:
public interface Resource extends InputStreamSource {
/**
* 资源是否存在.
*/
boolean exists();
/**
* 资源是否可读
*/
default boolean isReadable() {
return exists();
}
/**
* 资源是否处于打开状态
*/
default boolean isOpen() {
return false;
}
/**
* 是否为文件资源
*/
default boolean isFile() {
return false;
}
/**
* 获取资源对应的URL
*/
URL getURL() throws IOException;
/**
* 获取URI
*/
URI getURI() throws IOException;
/**
* 转换成文件
*/
File getFile() throws IOException;
/**
* 返回一个读的channel
*/
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的部分继承类图
Spring中资源的定位
在Spring中,使用ResourceLoader
对资源进行定位、加载,主要作用为:根据给定的资源文件地址返回对应的Resource。
public interface ResourceLoader {
/** Pseudo URL prefix for loading from the class path: "classpath:" */
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
/**
* 通过资源路径返回Resource对象
*/
Resource getResource(String location);
ClassLoader getClassLoader();
}
作为 Spring 统一的资源加载器,ResourceLoader
提供了统一的抽象,具体的实现则由相应的子类来负责实现,其类的部分类结构图如下:
DefaultResourceLoader
org.springframework.core.io.DefaultResourceLoader
是ResourceLoader
的默认实现,并且实现了资源加载器中重要的getResource(String)
方法,源码如下:
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
//通过ProtocolResolver加载资源
for (ProtocolResolver protocolResolver : this.protocolResolvers) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
URL url = new URL(location);
return new UrlResource(url);
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
可以看到默认的资源加载逻辑:
- 通过自定义的ProtocolResolver加载资源;
- 如果资源路径以"/"开头,将资源构造成
ClassPathContextResource
; - 如果资源路径以"classpath:"开头,将资源构造成
ClassPathResource
; - 资源路径既不以"/"开头又不以"classpath:"开头,则将资源构造成UrlResource。
ProtocolResolver
接口是资源加载的一个扩展点,允许用户自定义资源加载策略。Spring默认没有提供实现类,需要用户自行实现,并调用org.springframework.core.io.DefaultResourceLoader#addProtocolResolver
进行添加。
ResourcePatternResolver
ResourceLoader#getResource(String location)
仅支持单个资源的加载,ResourcePatternResolver
为我们提供了一次加载多个资源的功能。
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
ResourcePatternResolver
支持Ant模式通配符匹配,具体规则为:
“?”:匹配一个字符,如“config?.xml”将匹配“configA.xml”;
“*”:匹配零个或多个字符串,如“io/*/config.xml”将匹配“io/zgc/config.xml”,但不匹配匹配“io/config.xml”;而“io/config-*.xml”将匹配“io/config-dao.xml”;
“**”:匹配路径中的零个或多个目录,如“io/**/config.xml”将匹配“io/config.xml”,也匹配“io/zgc/spring/config.xml”;而“io/zgc/config-**.xml”将匹配“io/zgc/config-dao.xml”,即把“**”当做两个“*”处理。
PathMatchingResourcePatternResolver
是ResourcePatternResolver
最常用的一个子类,它加载资源的方法如下所示:
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
//是否以"classpath*:"开头
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// 字符串中是否含有"*"或者"?"
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()));
}
} else {
// Generally only look for a pattern after a prefix here,
// and on Tomcat only after the "*/" separator for its "war:" protocol.
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// a file pattern
return findPathMatchingResources(locationPattern);
} else {
// 根据ResourceLoader加载资源
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
参考(扩展阅读)
【死磕 Spring】—– IOC 之 Spring 统一资源加载策略
【第四章】 资源 之 4.4 Resource通配符路径 ——跟我学spring3
网友评论