Spring源码之ResourceLoader二:PathMatchingResourcePatternResolver实现getResources加载多文件

  • findAllClassPathResources走向
  • findPathMatchingResources走向
  • 单文件加载的getResource走向

上一篇文章 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,含有通配符,然后调用findPathMatchingResourcesif (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {return findPathMatchingResources(locationPattern);}else {//如果不是classpath*:开头,又不是打成war包,就说明是单个文件了,就交给getResourceLoader().getResource,其实也就是交给DefaultResourceLoaderreturn 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得到路径资源rootDirResourcesResource[] 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的协议是vfsresult.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));}else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {//判断rootDirUrl的协议是jarresult.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)做为文件夹的名称,//说到底就是通过从后截取,每次判断截取的字符串中是否含有通配符,如果没有,就说明是文件夹了,就返回configwhile (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的协议是vfsresult.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));}else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {//判断rootDirUrl的协议是jarresult.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提取匹配的文件,核心方法doRetrieveMatchingFilesSet<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:falseif (!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,含有通配符,然后调用findPathMatchingResourcesif (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {return findPathMatchingResources(locationPattern);}else {//如果不是classpath*:开头,又不是打成war包,就说明是单个文件了,就交给getResourceLoader().getResource,其实也就是交给DefaultResourceLoaderreturn new Resource[] {getResourceLoader().getResource(locationPattern)};}
}

继续,如果不是以"classpath*:"开头,可能打成war包,那就将war:这个前缀截取掉,然后判断剩下的是否含有通配符,如果是,那么就走findPathMatchingResources()。
如果既不是以"classpath*:"开头,有不是打成了war包,就说明是个单文件了,就如下,用到上一篇说的单文件加载的getResource:

单文件加载的getResource走向

else {//如果不是classpath*:开头,又不是打成war包,就说明是单个文件了,就交给getResourceLoader().getResource,其实也就是交给DefaultResourceLoaderreturn new Resource[] {getResourceLoader().getResource(locationPattern)};
}

至此,就讲完了多文件加载资源的处理。总结一下,其实就是三个走向
findPathMatchingResources()
findAllClassPathResources()
getResourceLoader().getResource。
其中findPathMatchingResources比较深些,大家对照源码多看几遍就搞懂了。

Spring源码之ResourceLoader(二):PathMatchingResourcePatternResolver实现getResources加载多文件相关推荐

  1. php源码自动识别文本中的链接,自动加载识别文件Auto.php

    用于本应用的控制器自动加载类设置,用法如同\CodeIgniter\Config\AutoloadConfig 自动加载识别文件:dayrui/App/应用目录/Config/Auto.php 语法格 ...

  2. ExoPlayer 源码阅读小记--HLS播放带缓存加载M38U文件过程

    基于ExoPlayer 2.17.1源码分析,基本是一边看一边写的流水账,记录下防止以后忘了: 第一步createMediaSource创建HlsMediaSource对象时同时会实例化出HlsPla ...

  3. Spring 源码分析(七)--bean的加载详细分析

    一:缓存中获取单例bean 前面已经提到过,单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例缓存中获取,当然这里也只是尝试加载,首先尝试从缓存中加载,然后再次尝试从sing ...

  4. Spring源码分析【3】-SpingWebInitializer的加载

    SpingWebInitializer的加载 Spring基于注解的配置代码: public class SpingWebInitializer extends AbstractAnnotationC ...

  5. Soul网关源码阅读(九)插件配置加载初探

    Soul网关源码阅读(九)插件配置加载初探 简介     今日来探索一下插件的初始化,及相关的配置的加载 源码Debug 插件初始化     首先来到我们非常熟悉的插件链调用的类: SoulWebHa ...

  6. Spring源码系列(十二)Spring创建Bean的过程(二)

    1.写在前面 上篇博客主要Spring在创建Bean的时候,第一次调用的Bean的后置处理器的过程,同时笔者也打算将整个Spring创建的Bean的过程,通过这个系列,将Bean的创建过程给讲清楚,废 ...

  7. idea 编译spring_《Spring源码解析(二)》构建 Spring5 源码工程,开启研读Spring源码之路...

    Spring5 源码下载注意事项 首先你的 JDK 需要升级到 1.8 以上.Spring3.0 开始,Spring 源码采用 github 托管,不再提供官网下载 链接.这里不做过多赘述,大家可自行 ...

  8. Spring源码分析(二):底层架构核心概念解析

    本节主要介绍一下Spring底层中用到的"基础设施",是后续看Spring源码所必备的,防止后续看源码的过程中,遇到不会的概念得单独跳出来学习. BeanDefinition Be ...

  9. Spring源码解析(二)BeanDefinition的Resource定位

    IOC容器的初始化过程主要包括BeanDefinition的Resource定位.载入和注册.在实际项目中我们基本上操作的都是ApplicationContex的实现,我们比较熟悉的ClassPath ...

最新文章

  1. Redis的缓存雪崩、缓存击穿、缓存穿透与缓存预热、缓存降级
  2. java param request_使用@RequestParam将请求参数绑定至方法参数
  3. LinuxWindows下批量修改文件后缀
  4. oracle和mysql通用建表语句_mysql建表语句到oracle怎么写?
  5. 课堂练习:eval()、in()
  6. 二分答案——木材加工(洛谷 P2440)
  7. opencv-api minEnclosingCircle
  8. python下载bt文件_python获取bt种子的详细信息
  9. php定时任务引入文件,php解决crontab定时任务不能写入文件问题的方法分析
  10. Centos 设置时区和时间以及增加中文输入法
  11. ios设置中性黑体_iOS 自定义-苹方字体的使用
  12. wireless-tools源码分析-iwlist
  13. 这是互联网变得越来越敌对的7种方式
  14. 新手看过来----讨厌的运算符
  15. Centos删除乱码文件或文件夹
  16. idea的debug功能详解
  17. 1.4 Git基本操作之删除文件找回及文件比较
  18. 国外黑客学习网站汇总
  19. 指尖轻舞桌面:Slide On Desk - 更新日志
  20. 《2019/04/12》java下载抖音视频

热门文章

  1. iOS12.4屏蔽系统更新的办法
  2. 国外LEAD靠这个写作工具来赚钱
  3. ubuntu安装无线网卡驱动(包括离线安装)
  4. STM32控制42步进电机
  5. 90后学生开发刷机精灵 身家千万买房赠父母
  6. 取得助工前发表的论文可以用来评中级吗?
  7. 用Python爬虫获取百度企业信用中企业基本信息!太厉害了!
  8. 进程用户态和内核态及其切换过程(转)
  9. linux truncate 命令,truncate 命令使用
  10. 网页制作 HTML基础结构、标记