Spring-Resource源码解析

7 篇文章 0 订阅
6 篇文章 0 订阅

本源码解析基于spring 1.5.6 版本

资源定义Resource

Spring 框架所有资源的抽象和访问接口,包括文件(xml以及其他),url,具体的classpath下的文件

public interface Resource extends InputStreamSource {
	//资源是否存在
	boolean exists();
	//是否可读
	default boolean isReadable() {
		return exists();
	}
	//资源所代表的句柄是否被一个 stream 打开了
	default boolean isOpen() {
		return false;
	}

	//是否为文件
	default boolean isFile() {
		return false;
	}

	//获取url
	URL getURL() throws IOException;

	//获取uri
	URI getURI() throws IOException;

	//获取文件
	File getFile() throws IOException;

	//返回ReadableByteChannel(包含close、open、read方法)
	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();

}

AbstractResource

AbstractResource实现了Resource的大部分方法

  • 获取文件
    默认会抛出异常,打印描述
@Override
public File getFile() throws IOException {
	throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
}
  • 是否存在
@Override
public boolean exists() {
	// Try file existence: can we find the file in the file system?
	try {
		return getFile().exists();
	}
	catch (IOException ex) {
		// Fall back to stream existence: can we open the stream?
		try {
			getInputStream().close();
			return true;
		}
		catch (Throwable isEx) {
			return false;
		}
	}
}

注意:我们需要实现AbstractResource来实现自定义Resource

资源加载ResourceLoader

public interface ResourceLoader {
	//值为classpath
	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;

	//获取resource
	Resource getResource(String location);

	@Nullable
	ClassLoader getClassLoader();
}

ResourceLoader的默认实现类DefaultResourceLoader

  • 获取类加载器
@Override
@Nullable
public ClassLoader getClassLoader() {
	return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}
  • 添加协议解析器
public void addProtocolResolver(ProtocolResolver resolver) {
	//非空校验
	Assert.notNull(resolver, "ProtocolResolver must not be null");
	this.protocolResolvers.add(resolver);
}
  • 类属性
@Nullable
private ClassLoader classLoader;

private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);
//缓存的Resource
private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap<>(4);
  • 获取Resource
1. 首先回去从本地协议解析器中解析资源
2. resolve方法是接口方法,必须实现这个方法.
3. 判断文件开头,如果是/开头就返回一个ClassPathContextResource,如果是classpath开头,那么返回一个ClassPathResource,如果不是以上述字符开头那么尝试网络资源,如果不是url那么继续返回ClassPathContextResource
@Override
public Resource getResource(String location) {
	//不允许传入空的位置
	Assert.notNull(location, "Location must not be null");
	//1.首先回去从本地协议解析器中解析资源,如果有就直接返回,resolve方法是接口方法,必须实现这个方法
	for (ProtocolResolver protocolResolver : this.protocolResolvers) {
		Resource resource = protocolResolver.resolve(location, this);
		if (resource != null) {
			return resource;
		}
	}
	//2.然后判断文件开头,如果是/开头就返回一个ClassPathContextResource
	if (location.startsWith("/")) {
		return getResourceByPath(location);
	}
	//3.如果是classpath开头,那么返回一个ClassPathResource
	else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
		return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
	}
	//如果不是以上述字符开头那么尝试网络资源
	else {
		try {
			// 尝试把location作为url解析
			URL url = new URL(location);
			return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
		}
		//如果不是url那么继续返回ClassPathContextResource
		catch (MalformedURLException ex) {
			// No URL -> resolve as resource path.
			return getResourceByPath(location);
		}
	}
}
/**
*	通过类加载器和路径,返回资源ClassPathContextResource
*/
protected Resource getResourceByPath(String path) {
	return new ClassPathContextResource(path, getClassLoader());
}

ProtocolResolver

  1. 用户可以自定义返回的Resource,需要实现接口
@FunctionalInterface
public interface ProtocolResolver {

	@Nullable
	Resource resolve(String location, ResourceLoader resourceLoader);

}
  1. 通过DefaultResourceLoader的addProtocolResolver注册进去
public void addProtocolResolver(ProtocolResolver resolver) {
	Assert.notNull(resolver, "ProtocolResolver must not be null");
	this.protocolResolvers.add(resolver);
}

使用示例

ResourceLoader resourceLoader = new DefaultResourceLoader();
String location="D:/work/mytext.txt";
Resource resource=resourceLoader.getResource(location);
location="/mytext.txt";
resource=resourceLoader.getResource(location);
location="www.baidu.com";
resource=resourceLoader.getResource(location);
//注意这里有问题,DefaultResourceLoader会返回UrlResource
//但是实际上不是,所以我们需要使用FileSystemResourceLoader
location="file:/Users/chenming673/Documents/spark.txt";
resource=resourceLoader.getResource(location);

FileSystemResourceLoader

由于默认的处理不是很好,spring提供了FileSystemResourceLoader并且重写了getResourceByPath方法

public class FileSystemResourceLoader extends DefaultResourceLoader {
	//path去掉/
	@Override
	protected Resource getResourceByPath(String path) {
		if (path.startsWith("/")) {
			path = path.substring(1);
		}
		return new FileSystemContextResource(path);
	}

	private static class FileSystemContextResource extends FileSystemResource implements ContextResource {
		//使用内部静态类返回FileSystemContextResource
		public FileSystemContextResource(String path) {
			super(path);
		}

		@Override
		public String getPathWithinContext() {
			return getPath();
		}
	}

}

FileSystemResource 类构造方法

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

StringUtils的cleanPath方法

public static String cleanPath(String path) {
	//如果没有长度,也就是path为空
	if (!hasLength(path)) {
		return path;
	}
	//	WINDOWS_FOLDER_SEPARATOR = "\\";
	//	FOLDER_SEPARATOR = "/";
	//	将字符串的windows的分隔符全部替换为/,详情见下方的replace源码分析
	String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);

	// 判断是否含有:
	int prefixIndex = pathToUse.indexOf(':');
	String prefix = "";
	if (prefixIndex != -1) {
		//如果有,获取前缀包含:
		prefix = pathToUse.substring(0, prefixIndex + 1);
		//如果前缀中含有/那么将前缀设置为空
		if (prefix.contains(FOLDER_SEPARATOR)) {
			prefix = "";
		}
		//如果前缀中不含有/,那么将pathToUse截取为冒号后面部分
		else {
			pathToUse = pathToUse.substring(prefixIndex + 1);
		}
	}
	//如果后面部分含有/那么将/放到前缀中
	if (pathToUse.startsWith(FOLDER_SEPARATOR)) {
		prefix = prefix + FOLDER_SEPARATOR;
		pathToUse = pathToUse.substring(1);
	}

	String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);
	LinkedList<String> pathElements = new LinkedList<>();
	int tops = 0;

	for (int i = pathArray.length - 1; i >= 0; i--) {
		String element = pathArray[i];
		if (CURRENT_PATH.equals(element)) {
			// Points to current directory - drop it.
		}
		else if (TOP_PATH.equals(element)) {
			// Registering top path found.
			tops++;
		}
		else {
			if (tops > 0) {
				// Merging path element with element corresponding to top path.
				tops--;
			}
			else {
				// Normal path element found.
				pathElements.add(0, element);
			}
		}
	}

	// Remaining top paths need to be retained.
	for (int i = 0; i < tops; i++) {
		pathElements.add(0, TOP_PATH);
	}
	// If nothing else left, at least explicitly point to current path.
	if (pathElements.size() == 1 && "".equals(pathElements.getLast()) && !prefix.endsWith(FOLDER_SEPARATOR)) {
		pathElements.add(0, CURRENT_PATH);
	}

	return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);
}

StringUtils的replace方法
其实就是将原字符串的所有的老字符替换为新字符
oldPattern为老字符串也就是被替换的字符串
newPattern为新字符串

public static String replace(String inString, String oldPattern, @Nullable String newPattern) {
	//如果其中一个参数为空的话,返回原来的字符串
	if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) {
		return inString;
	}
	//如果不含有老字符串,返回原来的字符串
	int index = inString.indexOf(oldPattern);
	if (index == -1) {
		return inString;
	}
	//计算原来字符串长度
	int capacity = inString.length();
	//如果替换的字符长度大于老字符长度,设置返回字符串的初始化长度为原字符串长度+16
	//如果新的替换字符大于老的字符,那么新得到的字符串长度也一定会比原来的长度大
	if (newPattern.length() > oldPattern.length()) {
		capacity += 16;
	}
	StringBuilder sb = new StringBuilder(capacity);
	//起始位置
	int pos = 0;  // our position in the old string
	//老字符串的长度
	int patLen = oldPattern.length();
	//如果存在老字符串,就继续循环
	while (index >= 0) {
		//拼接到sb中
		sb.append(inString.substring(pos, index));
		sb.append(newPattern);
		//起始位置向右移动当前下标加速老字符串的长度
		//也就是将字符串左右切分,左边部分为已替换,右边为为替换
		pos = index + patLen;
		//继续查找老字符串的位置下标
		index = inString.indexOf(oldPattern, pos);
	}
	//不存在老字符串了,但是最右边一部分还未拼接上,所以拼接一下
	sb.append(inString.substring(pos));
	return sb.toString();
}

delimitedListToStringArray方法
将字符串中除了分隔符部分,其余部分删除指定字符串

public static String[] delimitedListToStringArray(
			@Nullable String str, @Nullable String delimiter, @Nullable String charsToDelete) {
	
	if (str == null) {
		return new String[0];
	}
	if (delimiter == null) {
		return new String[] {str};
	}

	List<String> result = new ArrayList<>();
	//如果分割是空
	if (delimiter.isEmpty()) {
		for (int i = 0; i < str.length(); i++) {
			//如果只要是在charsToDelete存在的字符,都要除去掉
			//将除去了的字符串添加到结果集中
			//比如 http://abc.com charsToDelete="ac",那么得到的结果为http://b.com
			result.add(deleteAny(str.substring(i, i + 1), charsToDelete));
		}
	}
	else {
		int pos = 0;
		int delPos;
		//在pos后查询第一次出现的位置并赋值到delPos,即删除的位置
		while ((delPos = str.indexOf(delimiter, pos)) != -1) {
			//截取起始位置到删除位置,除去charsToDelete,并添加到结果集中
			result.add(deleteAny(str.substring(pos, delPos), charsToDelete));
			//重新赋值起始位置,删除位置加上分隔符长度
			pos = delPos + delimiter.length();
		}
		if (str.length() > 0 && pos <= str.length()) {
			//不存在指定分隔符后,将最后一部分去掉删除的字符串并放到结果中
			result.add(deleteAny(str.substring(pos), charsToDelete));
		}
	}
	return toStringArray(result);
}

ResourcePatternResolver

public interface ResourcePatternResolver extends ResourceLoader {

	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
	//返回Resource数组
	Resource[] getResources(String locationPattern) throws IOException;

}

PathMatchingResourcePatternResolver

getResources方法非常重要

  • 通配符匹配和精确匹配

通配符匹配顺序示例
classpath*:test/abc*/spring-*.xml

  1. 先通过findPathMatchingResources方法获取根节点classpath*:test/,然后再调用getResources,
  2. 根据协议类型,匹配通配符将资源放入Resource中
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
	@Override
	public Resource[] getResources(String locationPattern) throws IOException {
		Assert.notNull(locationPattern, "Location pattern must not be null");
		//	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
		if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
			//如果去掉前缀后的字符串包含*或者?,查询所有匹配的
			if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
				return findPathMatchingResources(locationPattern);
			}
			else {
				// 如果不是,全匹配
				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 {
				// a single resource with the given name
				return new Resource[] {getResourceLoader().getResource(locationPattern)};
			}
		}
	}
}
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
	//获取最前面没有*和?的字符串
	String rootDirPath = determineRootDir(locationPattern);
	//获取前缀后面的内容
	String subPattern = locationPattern.substring(rootDirPath.length());
	//递归调用根路径(不含通配符)
	Resource[] rootDirResources = getResources(rootDirPath);
	Set<Resource> result = new LinkedHashSet<>(16);
	for (Resource rootDirResource : rootDirResources) {
		//好像没什么卵用
		rootDirResource = resolveRootDirResource(rootDirResource);
		//获取url
		URL rootDirUrl = rootDirResource.getURL();
		//如果是bundle协议,那么久返回一个UrlResource
		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中去
			result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
		}
		//返回jar资源
		else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
			//满足通配符就加到result中去
			result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
		}
		else {
			//满足通配符就加到result中去
			result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
		}
	}
	if (logger.isTraceEnabled()) {
		logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
	}
	return result.toArray(new Resource[0]);
}

determineRootDir方法解析示例:

目标字符串结果字符串
classpath*:/abc/cc*/test-*.xmlclasspath*:/abc/
classpath*:/abc/cc/test-*.xmlclasspath*:/abc/cc/
protected String determineRootDir(String location) {
	int prefixEnd = location.indexOf(':') + 1;
	int rootDirEnd = location.length();
	//冒号后是否还有*和?
	while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
		//从长度-2的位置开始找最近的/并+1作为位置
		rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
	}
	if (rootDirEnd == 0) {
		rootDirEnd = prefixEnd;
	}
	//返回没有*和?的字符串
	return location.substring(0, rootDirEnd);
}

findAllClassPathResources方法

protected Resource[] findAllClassPathResources(String location) throws IOException {
	//判断classpath*:后是不是以/开头,如果是的那么将绝对路径的/去掉
	String path = location;
	if (path.startsWith("/")) {
		path = path.substring(1);
	}
	Set<Resource> result = doFindAllClassPathResources(path);
	if (logger.isTraceEnabled()) {
		logger.trace("Resolved classpath location [" + location + "] to resources " + result);
	}
	return result.toArray(new Resource[0]);
}

doFindAllClassPathResources通过类加载器获取所有的UrlResource

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
	Set<Resource> result = new LinkedHashSet<>(16);
	//获取类加载器
	ClassLoader cl = getClassLoader();
	//如果类加载器为空,使用系统的类加载器
	Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
	while (resourceUrls.hasMoreElements()) {
		URL url = resourceUrls.nextElement();
		result.add(convertClassLoaderURL(url));
	}
	if ("".equals(path)) {
		//如果为空,全加载进去
		addAllClassLoaderJarRoots(cl, result);
	}
	return result;
}

protected Resource convertClassLoaderURL(URL url) {
	return new UrlResource(url);
}

真正实现匹配的方法doRetrieveMatchingFiles

getPathMatcher()获取org.springframework.util.AntPathMatcher
调用doMatch(String, String, boolean, Map<String, String>)进行匹配

protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
		if (logger.isTraceEnabled()) {
			logger.trace("Searching directory [" + dir.getAbsolutePath() +
					"] for files matching pattern [" + fullPattern + "]");
		}
		for (File content : listDirectory(dir)) {
			String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
			if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
				if (!content.canRead()) {
					if (logger.isDebugEnabled()) {
						logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
								"] because the application is not allowed to read the directory");
					}
				}
				else {
					doRetrieveMatchingFiles(fullPattern, content, result);
				}
			}
			//通配符进行匹配
			if (getPathMatcher().match(fullPattern, currPath)) {
				result.add(content);
			}
		}
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值