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对象中的方法进行文件操作,例如获取输入流、判断资源是否存在、获取资源的文件名等