Spring Resource资源抽象和URL协议扩展

1,510 阅读5分钟

Spring对资源的扩展

Resource

  • Resource抽象类图,原java中只有对Url资源进行加载的。Spring对文件,Url,类路径下的资源进行整合抽象。

Resource类图

Follow Spring : Resource 由于Java标准的URL协议以及其扩展相对复杂,且本身API缺少了相关资源存在的判断等功能,所以Spring引入了自己的Resource抽象。

个人认为ResourceSpring对一些常用类型资源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实现,方法通过子类复写来使用其他类型

,例如FileSystemResourceClassRelativeContextResource

	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=自定义包名即可