本源码解析基于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
- 用户可以自定义返回的Resource,需要实现接口
@FunctionalInterface
public interface ProtocolResolver {
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);
}
- 通过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
- 先通过findPathMatchingResources方法获取根节点classpath*:test/,然后再调用getResources,
- 根据协议类型,匹配通配符将资源放入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-*.xml | classpath*:/abc/ |
classpath*:/abc/cc/test-*.xml | classpath*:/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);
}
}
}