Spring对资源的扩展
Resource
Resource抽象类图,原java中只有对Url资源进行加载的。Spring对文件,Url,类路径下的资源进行整合抽象。
Follow Spring : Resource 由于Java标准的
URL协议以及其扩展相对复杂,且本身API缺少了相关资源存在的判断等功能,所以Spring引入了自己的Resource抽象。个人认为
Resource是Spring对一些常用类型资源api的整合。给开发者一种通用的方式去访问各种类型资源,比如FileSystem,Classpath,UrlResource等。
如图
Resource : 包含了统一资源的访问方式。对于不同的资源来说,Resource有类似getFile,getUrl去应用不同种类的资源,其中FileSystemresource,ClassPathResource对应的就是文件系统和类路径下的实现。
ResourceLoader
类图如下:
以及接口申明
public interface ResourceLoader {
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // classpath前缀常量
//根据不同的位置字符串来获取资源
Resource getResource(String location);
//获取类加载器,为什么需要类加载器?因为需要获取ClassPath/FileSystem的资源。
ClassLoader getClassLoader();
}
接口默认实现DefaultResoueceLoader
/**
* 默认获取Resource
* @param location 资源路径
* @return Resource
*/
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
//使用协议解析器解析路径参数
//1. 通过实现ProtocolResolver,并且通过DefaultClassLoader#addProtocolResolver()添加
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
//2. 若 '/'开头,则通过不同应用上下文来返回 ClassPathContextResource/FileSystemContextResource/ServletContextResource
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 {
// Try to parse the location as a URL...
URL url = new URL(location);
//如果是FileUrl那么则返回FileUrlResource,反之则返回UrlResource.
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
-
优先使用自定义的
ProtocolResolver来加载资源 -
以
/开头的路径参数则进入getResourceByPath(String location),根据不同的应用上下文来进行不同资源获取 -
以
classpath:开头则返回ClassPathResource -
其他则当成
url,是文件则使用FileUrlResource,否则则使用UrlResource,这里注意,如果不是文件或者网络url链接,那么则会抛出MalformedURLException异常,从而执行getResourceByPath,eg : test/a.xml,这个路径会进入到getResourceByPath
核心方法getResourceByPath(String location)
默认通过
ClassPathContextResource实现,方法通过子类复写来使用其他类型,例如
FileSystemResource、ClassRelativeContextResource
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
ContextResource
是
Resource的辅助增强接口,通过额外的getPathWithinContext在例如ServletContext之类的应用上下文中获取相对上下文根目录的相对路径。在文件或者类路径下,也通用。FileSystemContextResource,ClassRelativeContextResource等。
ResourcePatternResolver
资源模板解析器,作用是一次性解析多个资源。通过
ant风格的url来进行解析
public interface ResourcePatternResolver extends ResourceLoader {
//携带该前缀会查询所有jar中的
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
//获取资源集合
Resource[] getResources(String locationPattern) throws IOException;
}
PathMatchingPatternResourceResolver
是
ResourcePatternResolver的默认实现类
其中核心为getResources()
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
//1. 是否classpath* 开头
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// 2. 是否是ant风格路径 (带*的多路径匹配)
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
// 2.1 进行模式匹配查找资源
return findPathMatchingResources(locationPattern);
}
else {
//3. 没有通配符则进行类路径资源获取 (使用ClassLoader返回所有类路径和jar中对应的资源)
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
//处理前缀
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
//4. 非classPath资源的通配符资源获取
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
return findPathMatchingResources(locationPattern);
}
else {
//5. 单个resource资源获取
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
核心步骤:
- 若是classpath* 标记的path , 进行
Ant模式匹配,若匹配上,则进行匹配资源查找 - 没有通配符则进行ClassLoader加载对应路径资源
- 若不是classpath标记,且是ant风格,则进行模式匹配
- 若不是ant风格,则使用DefaultResourceLoader进行资源加载
其中几个核心的方法
findPathMatchingResources(String locationPattern): 该方法用来解析携带Ant风格Url资源的解析
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern); //从一个ant风格的url中获取其根目录 classpath*:/WEB-INF/*.xml -> classpath*:/WEB-INF/
String subPattern = locationPattern.substring(rootDirPath.length()); //获取除了根目录的字串 -> *.xml
Resource[] rootDirResources = getResources(rootDirPath); //先获取根目录的资源集合
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource); //给子类机会去修改该资源加载
URL rootDirUrl = rootDirResource.getURL();
//bundle资源解析 WebSphere
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文件解析
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
//jar路径解析,解析jar中所有的entry
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
//将解析的结果加入LinkedHashSet中
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
//FileSystem路径解析: 在根目录下递归进行匹配填充
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
return result.toArray(new Resource[0]);
}
findAllClassPathResources(String location): 该方法则是用来加载类路径下的具体资源
protected Resource[] findAllClassPathResources(String location) throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
//使用类加载器加载相同路径的资源,这里如果path=''那么将加载所有ClassLoader下面的jar
Set<Resource> result = doFindAllClassPathResources(path);
if (logger.isTraceEnabled()) {
logger.trace("Resolved classpath location [" + location + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
java URL协议的扩展
我们常见的Java中的URL协议有,http,https,ftp,file等。那么如何基于java.net.URL进行协议扩展呢?
URL#setURLStreamHandlerFactory全局设置协议工厂- 实现
Handler,并在sun.net.www.protocol.{协议名}.Handler固定位置下 - 实现
Handler,并通过-Djava.protocol.handler.pkgs指定自定义协议包名
相关实现代码如下
自定义协议工厂方式
//1. 自定义名为 lazylittle的协议并继承URLStreamHandlerFactory
public class LazylittleStreamHandlerFactory implements URLStreamHandlerFactory {
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
return new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
return new LazylittleUrlConnection(u);
}
};
}
//2. 继承URLConnection
static class LazylittleUrlConnection extends URLConnection {
private ClassPathResource classPathResource;
//3. 委派给ClasspathResource来实现
protected LazylittleUrlConnection(URL url) {
super(url);
if (!url.toString().startsWith("lazylittle")) {
throw new UnsupportedOperationException("invalid prefix " + url);
}
//delegate
classPathResource = new ClassPathResource(url.getPath());
}
@Override
public InputStream getInputStream() throws IOException {
return classPathResource.getInputStream();
}
@Override
public void connect() throws IOException {
}
}
演示代码
//1. 最优先的且会覆盖之后的策略,ClassLoader全局的!
URL.setURLStreamHandlerFactory(new LazylittleStreamHandlerFactory());
URL url = new
URL("lazylittle:/spring/in/action/resource/custom/LazylittleStreamHandlerFactory.class");
IoUtil.copy(url.openStream(), System.out); //输出到控制台
固定包下实现
通过实现URLStreamHandler,然后放置在
sun.net.www.protocol.{自定义协议名}下即可,相关实现看上面的工厂实现即可
指定-D自定义包名参数
- 在已定义包下实现
URLStreamHandler - 在启动参数上
-Djava.protocol.handler.pkgs=自定义包名即可