Spring源码之ResourceLoader二:PathMatchingResourcePatternResolver实现getResources加载多文件
上一篇文章
Spring源码之ResourceLoader(一):实现类DefaultResourceLoader实现getResource写了Spring容器加载多文件委托给了PathMatchingResourcePatternResolver,
PathMatchingResourcePatternResolver针对多文件的情况有三种走向分别是:
findPathMatchingResources()
findAllClassPathResources()
getResourceLoader().getResource
下面我们来分析其源码实现。
废话不多说上源码:
@Override
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null"); //断言,路径不能传空
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
//CLASSPATH_ALL_URL_PREFIX:"classpath*:",判断是否是classpath*:开头
//getPathMatcher()就是我们AntPathMatch。isPattern就是判断这个字符串里有没有*,?,{和}这些通配符
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
//带有通配符走这里
return findPathMatchingResources(locationPattern);
}
else {
//没有通配符走这里,也就是直接通过路径就能找到,不用去匹配通配符
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));//将classpath*:去掉
}
}
else {
//如果不是以"classpath*:"开头,就会走下面这个
//可能是打成war包,将这个前缀拿出来。留下":"后面的
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
//把前缀去掉后再进行一次判断是不是isPattern,含有通配符,然后调用findPathMatchingResources
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
return findPathMatchingResources(locationPattern);
}
else {
//如果不是classpath*:开头,又不是打成war包,就说明是单个文件了,就交给getResourceLoader().getResource,其实也就是交给DefaultResourceLoader
return new Resource[] {
getResourceLoader().getResource(locationPattern)};
}
}
}
首先,判断是不是以”classpath*:”开头,这种开头就说明会匹配多个资源文件,如果是,就下面操作
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
//getPathMatcher()就是我们AntPathMatch。isPattern就是判断这个字符串里有没有*,?,{和}这些通配符
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
//带有通配符走这里
return findPathMatchingResources(locationPattern);
}
else {
//没有通配符走这里,也就是直接通过路径就能找到,不用去匹配通配符
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));//将classpath*:去掉
}
}
可以看到,将”classpath*:”截取掉,然后判断剩下的是不是一个Pattern模式串,也就是是否含有*,?,{和}这些通配符。getPathMatcher()返回的是this.pathMatcher,而this.pathMatcher其实就是我们前面系列文章Spring源码之AntPathMatcher(一):doMatch算法中讲的AntPathMatcher。
public PathMatcher getPathMatcher() {
return this.pathMatcher;
}
private PathMatcher pathMatcher = new AntPathMatcher();
进入getPathMatcher().isPattern()可以看到,就是判断剩下的字符串中是否含有*,?,{和}这些通配符:
public boolean isPattern(@Nullable String path) {
if (path == null) {
return false;
} else {
boolean uriVar = false;
for(int i = 0; i < path.length(); ++i) {
char c = path.charAt(i);
if (c == '*' || c == '?') {
return true;
}
if (c == '{') {
uriVar = true;
} else if (c == '}' && uriVar) {
return true;
}
}
return false;
}
}
如果含有通配符,说明是一个Pattern模式串,会去匹配所有符合模式串的路径资源。比如”classpath*: *.properties”会匹配加载所有classpath下的properties文件。这种情况下,调用findPathMatchingResources(locationPattern)处理。也就是findPathMatchingResources走向,如果剩下的字符串中没有通配符,也就是直接通过路径就能找到,那我们就走到findAllClassPathResources方法,这里我们先说下没有通配符的情况,也就是findAllClassPathResources走向
findAllClassPathResources走向
//没有通配符走这里,也就是直接通过路径就能找到,不用去匹配通配符
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));//将classpath*:去掉
继续,进入findAllClassPathResources方法中
protected Resource[] findAllClassPathResources(String location) throws IOException {
//传入"config/"
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
Set<Resource> result = doFindAllClassPathResources(path);//通过jdk的classpathLoader去加载路径
if (logger.isTraceEnabled()) {
logger.trace("Resolved classpath location [" + location + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
可以看到,如果这剩下的字符串是以“/”开头的,就将“/”截取掉,然后将路径字符串传给doFindAllClassPathResources,我们再进入doFindAllClassPathResources
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
//通过jdk的classpathLoader去加载路径,得到个Set<Resource>
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;
}
可以看到,是通过jdk的classpathLoader去加载路径,得到一个枚举,然后去遍历这个枚举,将其中元素转换为UrlResource。返回Resource的set集合
protected Resource convertClassLoaderURL(URL url) {
return new UrlResource(url);
}
最后回到findAllClassPathResources,将doFindAllClassPathResources通过jdk的classpathLoader加载路径而得到的结果转换为Resource数组
return result.toArray(new Resource[0]);
以上就是截取掉”classpath*:“之后剩下的字符不含有通配符的处理。
下面我们讲当剩下的字符串中含有通配符,Spring是怎么处理的。
也就是
findPathMatchingResources走向
return findPathMatchingResources(locationPattern);
好,继续,看findPathMatchingResources内部的实现:
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern);//获取rootDirPath,其实就是取出前面不含有通配符的字符串,比如"classpath*:config/aa/*.properties"返回classpath*:config/这个文件夹的名称
String subPattern = locationPattern.substring(rootDirPath.length());//从rootDirPath往后截断,比如"classpath*:config/aa/*.properties",截断后是"*.properties"
//这个地方再去调用getResources,就用到了递归算法,rootDirPath就是上面的得到的是不含有通配符的,所以会走 findAllClassPathResources,不会走findPathMatchingResources,也就不会死循环
//最终调用findAllClassPathResources得到路径资源rootDirResources
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
//遍历这个路径资源
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();//得到url。比如我的是file:/Users/chenzhe/博客写作/攻城掠地/1SpringFamily/Spring/antmatchpath/target/classes/config/
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
//判断rootDirUrl的协议是不是bundle开头的这个对应的协议
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
//判断rootDirUrl的协议是vfs
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
//判断rootDirUrl的协议是jar
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
首先,第一句
String rootDirPath = determineRootDir(locationPattern);
进去determineRootDir方法
protected String determineRootDir(String location) {
//位置字符串location:"classpath*:config/*.properties"
int prefixEnd = location.indexOf(':') + 1;//prefixEnd:11,也就是:的位置是第11,
int rootDirEnd = location.length();//rootDirEnd:30,也就是location的长度为30
//getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd)判断剩下的是不是Pattern模式串,也就是判断是否含有*,**,?等字符
//其实就是剩下的config/*.properties是否含有通配符,如果含有,就通过"/"截断,就是从后往前一个循环,一直截取到剩下的"/"以前的里面不含有通配符就停止
//在我们这个例子中就是当截到"config/",这个时候里面没有通配符了,就停止了,然后取(0到rootDirEnd)做为文件夹的名称,
//说到底就是通过从后截取,每次判断截取的字符串中是否含有通配符,如果没有,就说明是文件夹了,就返回config
while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;//lastIndexOf截取/前面的字段
}
if (rootDirEnd == 0) {
rootDirEnd = prefixEnd;
}
return location.substring(0, rootDirEnd);//返回classpath*:config/这个文件夹的名称
}
这里大家可以看我注释,其实就是先将“:”后面的截取掉,剩下的字符再经过循环截取“/”前面的,直到截取“/”分隔符所得到的字符串里面没有通配符,最终返回的是“:”后面的只是路径的,没有通配符的字符串。
这里举个例子就更清楚了,比如下面这个url串:“classpath*:config/aa/**/cc/dd/.properties”。determineRootDir方法先截取“:”后面的也就剩下字符串”config/aa/**/cc/dd/.properties”。然后后面循环的第一次得到“/”之前的字符串”config/aa/**/cc/dd/”,这时候发现这字符串中含有通配符”*”,就继续从后往前截取,循环,直到找到“config/aa/”。所以determineRootDir其实就是找到”classpath*:“后面不含通配符的路径。
好,再回到findPathMatchingResources继续下一句
String subPattern = locationPattern.substring(rootDirPath.length());
subPattern得到的显而易见,就是上面rootDirPath之后含有通配符的路径字符串,这字符串后面会用到,是做为一个pattren模式串参数传给其他方法,用于匹配算法的,后面我们再说。
继续,下一句:
Resource[] rootDirResources = getResources(rootDirPath);
你会看到,又调用了getResources方法,这里就是一个递归算法了,因为传入的参数是rootDirPath,这个上文中说了,得到的是”classpath*:“后面不含通配符的路径字符串,所以也就不会走findPathMatchingResources,也就不会死循环了,而是走else里面的findAllClassPathResources了:
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
//带有通配符走这里
return findPathMatchingResources(locationPattern);
}
else {
//没有通配符走这里,也就是直接通过路径就能找到,不用去匹配通配符
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));//将classpath*:去掉
}
findAllClassPathResources的情况我们上面说过了,所以继续如下:
for (Resource rootDirResource : rootDirResources) {
//遍历这个路径资源
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();//得到url。比如我的是file:/Users/chenzhe/博客写作/攻城掠地/1SpringFamily/Spring/antmatchpath/target/classes/config/
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
//判断rootDirUrl的协议是不是bundle开头的这个对应的协议
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
//判断rootDirUrl的协议是vfs
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
//判断rootDirUrl的协议是jar
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
可以看到,首先遍历这个rootDirResources,这个rootDirResources通过上面我们可以了解到,其实是符合某一模式串的一组路径资源,所以,比如“classpath*:config/*properties”,得到的rootDirResources就是符合config文件夹下的一组properties的路径资源,
下面:
URL rootDirUrl = rootDirResource.getURL();//得到url。比如我的是file:/Users/chenzhe/博客写作/攻城掠地/1SpringFamily/Spring/antmatchpath/target/classes/config/
得到url比如我的例子就得到的是
file:/Users/chenzhe/博客写作/攻城掠地/1SpringFamily/Spring/antmatchpath/target/classes/config/
然后下面几个if else就是判断rootDirUrl的协议,也就是file还是jar还是vfs等等,
最后,我们的例子走的是
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
继续,进入doFindPathMatchingFileResources,返回的是:
return doFindMatchingFileSystemResources(rootDir, subPattern);//去文件系统里查找资源
继续,进入doFindMatchingFileSystemResources,调用了方法retrieveMatchingFiles:
protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
//去文件系统里查找资源
if (logger.isTraceEnabled()) {
logger.trace("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
}
Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);//retrieveMatchingFiles提取匹配的文件,核心方法doRetrieveMatchingFiles
Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());//拿到
for (File file : matchingFiles) {
//得到匹配的文件
result.add(new FileSystemResource(file));//用FileSystemResource包装成符合文件系统的Resource,
}
return result;//返回结果
}
继续,进入retrieveMatchingFiles方法,其核心方法是doRetrieveMatchingFiles:
doRetrieveMatchingFiles(fullPattern, rootDir, result);//核心方法
继续,进入doRetrieveMatchingFiles,可以看到是一个基于文件系统的遍历做目录或者文件比较,然后getPathMatcher().match(fullPattern, currPath),这个是我们系列文章中讲的match匹配算法:Spring源码之AntPathMatcher(一):doMatch算法。最后返回匹配成功的结果:
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 + "/")) {
//matchStart和之前的match区别就是matchStart是一个非完整路径,fullMatch:false
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)) {
//match方法熟悉了吧
result.add(content);
}
}
}
这里比较深,大家一边对照源码一边看就清楚了。
回过头来findPathMatchingResources,可以看到得到的result最终和findAllClassPathResources一样,转换成了Resource的数组
return result.toArray(new Resource[0]);
好,再回到getResources方法中,继续,如果不是以”classpath*:”开头,就走下面的else:
else {
//如果不是以"classpath*:"开头,就会走下面这个
//可能是打成war包,将这个前缀拿出来。留下":"后面的
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
//把前缀去掉后再进行一次判断是不是isPattern,含有通配符,然后调用findPathMatchingResources
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
return findPathMatchingResources(locationPattern);
}
else {
//如果不是classpath*:开头,又不是打成war包,就说明是单个文件了,就交给getResourceLoader().getResource,其实也就是交给DefaultResourceLoader
return new Resource[] {
getResourceLoader().getResource(locationPattern)};
}
}
继续,如果不是以”classpath*:”开头,可能打成war包,那就将war:这个前缀截取掉,然后判断剩下的是否含有通配符,如果是,那么就走findPathMatchingResources()。
如果既不是以”classpath*:”开头,有不是打成了war包,就说明是个单文件了,就如下,用到上一篇说的单文件加载的getResource:
单文件加载的getResource走向
else {//如果不是classpath*:开头,又不是打成war包,就说明是单个文件了,就交给getResourceLoader().getResource,其实也就是交给DefaultResourceLoader
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
至此,就讲完了多文件加载资源的处理。总结一下,其实就是三个走向
findPathMatchingResources()
findAllClassPathResources()
getResourceLoader().getResource。
其中findPathMatchingResources比较深些,大家对照源码多看几遍就搞懂了。
今天的文章Spring源码之ResourceLoader(二):PathMatchingResourcePatternResolver实现getResources加载多文件分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/26307.html