1.获取资源对象
ApplicationContext接口是BeanFactory的子接口,意味着它扩展了BeanFactory的功能,其中继承ResourcePatternResolver接口,提供获取Resource资源的功能,示例如下:
@SpringBootApplication
public class A01 {
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(A01.class, args);
// 获取类路径下及所有jar包下的spring.factories文件资源
Resource[] resources = context.getResources("classpath*:META-INF/spring.factories");
for (Resource resource : resources) {
System.out.println(resource);
}
}
}
2.获取资源原理
这里的context是AnnotationConfigServletWebServerApplicationContext,调用父类GenericApplicationContext的getResources获取资源,源码如下:
public Resource[] getResources(String locationPattern) throws IOException {
// resourceLoader默认为null
if (this.resourceLoader instanceof ResourcePatternResolver) {
return ((ResourcePatternResolver) this.resourceLoader).getResources(locationPattern);
}
return super.getResources(locationPattern);
}
默认情况下会调用父类AbstractApplicationContext#getResources
public Resource[] getResources(String locationPattern) throws IOException {
// resourcePatternResolver在构造函数中设置的PathMatchingResourcePatternResolver
return this.resourcePatternResolver.getResources(locationPattern);
}
PathMatchingResourcePatternResolver#getResources源码如下:
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
// 如果以classpath*:开头
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// 调用AntPathMatcher#isPattern判断是查找多个文件还是单个文件,即判断是否包含*、?或者{}
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
// 处理带通配符的路径
return findPathMatchingResources(locationPattern);
}
else {
// 通过是单个文件,则查找所有路径下的资源,包括jar包中的资源
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
// 获取路径前缀
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
// 去掉前缀后,调用AntPathMatcher#isPattern判断是否包含通配符*、?或者{}
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// 处理带通配符的路径
return findPathMatchingResources(locationPattern);
}
else {
// 调用DefaultResourceLoader#getResource获取单个资源
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
PathMatchingResourcePatternResolver在获取资源时有3种可能:
1)findPathMatchingResources处理带通配符的路径
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
// 获取不带通配符的目录地址
String rootDirPath = determineRootDir(locationPattern);
// 路径中带通配符的部分
String subPattern = locationPattern.substring(rootDirPath.length());
// 递归调用,获取不带通配符目录的资源,会走findAllClassPathResources或者DefaultResourceLoader的逻辑
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
// 遍历所有目录资源
for (Resource rootDirResource : rootDirResources) {
// 获取目录的地址
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();
// 处理OSGI相关的资源
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文件资源
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
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]);
}
处理普通文件资源doFindPathMatchingFileResources中主要调用了doFindMatchingFileSystemResources(rootDir, subPattern),逻辑如下:
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);
Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
// 包装成FileSystemResource返回
for (File file : matchingFiles) {
result.add(new FileSystemResource(file));
}
return result;
}
retrieveMatchingFiles中核心方法是doRetrieveMatchingFiles,逻辑如下:
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);
}
}
}
路径匹配算法在AntPathMatcher#doMatch中实现,源码如下:
protected boolean doMatch(String pattern, @Nullable String path, boolean fullMatch,
@Nullable Map<String, String> uriTemplateVariables) {
// 如果path为空,匹配失败
// 如果path与pattern不是都以/开头,或都不以/开头,则匹配失败
if (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
return false;
}
// 根据/分割pattern字符串
String[] pattDirs = tokenizePattern(pattern);
// 如果是全匹配,则调用isPotentialMatch粗略判断下是否匹配
if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) {
return false;
}
// 根据/分割path字符串
String[] pathDirs = tokenizePath(path);
int pattIdxStart = 0;
int pattIdxEnd = pattDirs.length - 1;
int pathIdxStart = 0;
int pathIdxEnd = pathDirs.length - 1;
// 从前往后遍历两个分割后的数组
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String pattDir = pattDirs[pattIdxStart];
// 如果在pattDirs中找到了**,则退出循环,因为**可以匹配多级路径
if ("**".equals(pattDir)) {
break;
}
// 如果pattDir和pathDirs中不匹配,则直接返回false
if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
return false;
}
// 两个字符串数组索引同步向后移动
pattIdxStart++;
pathIdxStart++;
}
// 如果path数组已经遍历完成
if (pathIdxStart > pathIdxEnd) {
// 如果pattern数组也遍历完成,需要比较不是都以/开头,或都不以/开头
// 需要比较的原因是tokenizePattern方法会忽略前后的/
// 例如:"/aa/bb/*", "/aa/bb/cc"
// 例如:"/aa/bb/*/", "/aa/bb/cc/"
if (pattIdxStart > pattIdxEnd) {
return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator));
}
// 如果不是匹配整个字符串,即匹配前缀部分,则匹配成功
if (!fullMatch) {
return true;
}
// 如果pattern字符串为xx/*且path字符串为xx/,则匹配成功
// 例如:pattern为/aa/bb/cc/*或/aa/bb/cc/*/,path为/aa/bb/cc/
if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
return true;
}
// 如果pattern的路径层级更多,则多出的部分必须是**才能匹配上
// 例如:"/aa/bb/cc/**/**", "/aa/bb/cc"
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
else if (pattIdxStart > pattIdxEnd) {
// 如果path数组未遍历完成,但pattern数组已遍历完成,则返回false
// 例如:"/aa/*/", "/aa/bb/cc"
return false;
}
else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
// 如果不是匹配整个字符串,并且pattern中有**,则返回true
return true;
}
// 从后往前遍历两个分割后的数组,找到最后一个**
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String pattDir = pattDirs[pattIdxEnd];
// 如果在pattDirs中找到了**,则退出循环,因为**可以匹配多级路径
if (pattDir.equals("**")) {
break;
}
// 如果pattDir和pathDirs中不匹配,则直接返回false
if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
return false;
}
// 两个字符串数组索引同步向后移动
pattIdxEnd--;
pathIdxEnd--;
}
// 如果path路径已经遍历完成,则pattern中间部分必须全是**才能匹配上
if (pathIdxStart > pathIdxEnd) {
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
// 匹配第一次**与最后一次**中间的部分
while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
int patIdxTmp = -1;
// 从第一次**+1的索引处开始遍历,找到下一个**
for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
if (pattDirs[i].equals("**")) {
patIdxTmp = i;
break;
}
}
if (patIdxTmp == pattIdxStart + 1) {
// 如果是**/**的情况,继续循环
pattIdxStart++;
continue;
}
// 计算出pattern两个**之间的路径个数
int patLength = (patIdxTmp - pattIdxStart - 1);
// 计算path中间的路径个数
int strLength = (pathIdxEnd - pathIdxStart + 1);
int foundIdx = -1;
// 判断pattern中**和下一个**中间固定路径是否匹配
strLoop:
for (int i = 0; i <= strLength - patLength; i++) {
for (int j = 0; j < patLength; j++) {
// pattern第一个需要匹配的字符串
String subPat = pattDirs[pattIdxStart + j + 1];
String subStr = pathDirs[pathIdxStart + i + j];
// j始终为0,path不断向后平移,直到找到能匹配的路径
if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
continue strLoop;
}
}
// 记录path平移的位置
foundIdx = pathIdxStart + i;
break;
}
// 如果平移没有匹配上,返回false
if (foundIdx == -1) {
return false;
}
// pattIdxStart设置为下一个**的位置
pattIdxStart = patIdxTmp;
// pathIdxStart设置为匹配上的字符串的下一个字符串
pathIdxStart = foundIdx + patLength;
}
// 上述情况都匹配完成后,如果pattern还有剩余数据,则必须全部是**,否则不匹配
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
2)findAllClassPathResources处理Classpath:*下的不带通配符路径
protected Resource[] findAllClassPathResources(String location) throws IOException {
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]);
}
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
// 通过类加载器获取path的资源
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
// 转换成UrlResource对象
result.add(convertClassLoaderURL(url));
}
// 如果path为空,即原始路径为classpath*:/,添加所有jar包路径
if (!StringUtils.hasLength(path)) {
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
类加载器获取path的资源源码如下:
public Enumeration<URL> getResources(String name) throws IOException {
// tmp[0]存放父加载器查找的URL,tmp[1]存放当前类加载器查找的URL
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = getBootstrapResources(name);
}
tmp[1] = findResources(name);
// 嵌套的迭代器
return new CompoundEnumeration<>(tmp);
}
ClassLoader的getResources方法中会递归查找父类加载器加载的资源,返回CompoundEnumeration对象是一个嵌套的迭代器,内部还封装了Enumeration迭代器,因此父类查找到的资源会放入内部的迭代器中,层层嵌套
3)DefaultResourceLoader获取单个资源
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
// 获取协议解析器,默认为空,可以添加自定义的协议解析器,如果有添加协议解析器,则遍历
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
// 使用协议解析器解析路径获取对应的资源
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
// 如果路径以/开头,则调用getResourceByPath,默认是返回ClassPathContextResource对象
// 但是web环境下ServletWebServerApplicationContext重写了该方法会返回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 {
// 尝试将路径解析为URL对象
URL url = new URL(location);
// 如果是文件协议,则返回FileUrlResource对象,否则返回UrlResource对象
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// 如果不能解析成URL对象,则调用getResourceByPath
return getResourceByPath(location);
}
}
}
通过getResources获取到资源文件后,就可以使用Resource对象中的方法进行文件操作,例如获取输入流、判断资源是否存在、获取资源的文件名等