【Spring源码解析】- Resource资源加载体系

前言

在Spring当中的资源和加载是分开定义的,为什么会这样做呢?

  1. 各干各的活也就是职能划分问题,换句话说怎么加载资源,跟我是个什么资源不搭边
  2. 接口各自能做一个规范,也就是我提供大体上的框架,谁也不能超纲

那么为什么Spring 需要单独实现处理资源的方式呢?我们看下官方文档怎么描述的
官方描述
英语不好,大概齐的意思是我想用java.net.url 但是他资源加载太拉跨了,各种想要的加载方式他都不太行,然后这忍了就算了,但是功能有残缺就忍不了了

资源:Resource

我们看一下 Resource 的整体结构,和一些比较重要的方法

public interface Resource extends InputStreamSource {

		/**
		 * 返回一个 boolean 指示此资源是否以物理形式实际存在
		 */
		boolean exists();

		/**
		 * 资源是否可读
		 */
		boolean isReadable();

		/**
		 返回一个 boolean ,指示此资源是否表示具有打开流的句柄。
		 如果为 ,则 true InputStream 无法多次读取,必须仅读取一次,然后关闭以避免资源泄漏。
		 返回 false 所有常用资源实现,但 除外 InputStreamResource
		 */
		boolean isOpen();

		/**
		 * 是否为 File
		 */
		boolean isFile();
		/**
		 * 返回资源的 URL
		 */
		URL getURL() throws IOException;
		/**
		 * 返回资源的 URL
		 */
		URI getURI() throws IOException;
		/**
		 * 返回资源的 File句柄
		 */
		File getFile() throws IOException;

		/**
		 * 返回一个可以读取字节的通道
		 */
		ReadableByteChannel readableChannel() throws IOException;

		long contentLength() throws IOException;

		long lastModified() throws IOException;

		/**
		 * 相对路径中获取资源
		 */
		Resource createRelative(String relativePath) throws IOException;
		/**
		 * 获得资源名称
		 */
		String getFilename();

		/**
		 *返回此资源的说明,用于在处理资源时输出错误。这通常是资源的完全限定文件名或实际 URL
		 */
		String getDescription();
	}

可以看到继承了 InputStreamSource 接口,而 InputStreamSource 接口只有一个方法:getInputStream, 方法是 Resource核心功能,用于获取资源的内容。

public interface InputStreamSource {
	/**
	 *查找并打开资源,返回 for InputStream 从资源读取。预计每次调用都会返回一个新的 InputStream .调用方负责关闭流。
	 */
	InputStream getInputStream() throws IOException;
}

为什么会有每次调用都会返回一个新的InputStream的说明呢,因为大部分InputStream 是没有实现的reset();方法的

继承关系

接着看下他的继承关系
继承关系
然后看下具体实现:

  • UrlResource :包装了java.net.URL 并可用于访问通常可通过 URL 访问的任何对象
  • ClassPathResource :从类路径获取的资源
  • FileSystemResource :对 java.io.File 类型资源的封装,支持nio2 Api的支持
  • ServletContextResource : Web 应用程序根目录中的相对路径的资源
  • InputStreamResource :将给定的 InputStream 作为一种资源的 Resource 的实现类
  • ByteArrayResource :字节数组提供的数据的封装。内部会new 一个ByteArrayInputStream

很明显,AbstractResource 是默认实现,他的内部提供的大量的公共已实现的公共方法,要不然咋能抽象出来它吗?

比较典型的exists() 方法:子类想要实现的就自己动手实现,懒的实现的只要把关键方法实现了就行

@Override
	public boolean exists() {
		// 看看是不是文件,能不能在文件系统中找到文件
		// 基于 File 进行判断
		if (isFile()) {
			try {

				return getFile().exists();
			}
			catch (IOException ex) {
				debug(() -> "Could not retrieve File for existence check of " + getDescription(), ex);
			}
		}
		// // 基于 InputStream 进行判断
		try {

			getInputStream().close();
			return true;
		}
		catch (Throwable ex) {
			debug(() -> "Could not retrieve InputStream for existence check of " + getDescription(), ex);
			return false;
		}
	}

再继续来看下哪些方法提供了默认实现:

	@Override
	public URI getURI() throws IOException {
		URL url = getURL();
		try {
			return ResourceUtils.toURI(url);
		}
		catch (URISyntaxException ex) {
			throw new IOException("Invalid URI [" + url + "]", ex);
		}
	}
	/**
	 * 根据 getInputStream() 构建 ReadableByteChannel
	 */
	@Override
	public ReadableByteChannel readableChannel() throws IOException {
		return Channels.newChannel(getInputStream());
	}
	/**
	 * 获取资源的长度
	 * 实际就是资源的字节长度
	 */
	@Override
	public long contentLength() throws IOException {
		InputStream is = getInputStream();
		try {
			long size = 0;
			byte[] buf = new byte[256];
			int read;
			while ((read = is.read(buf)) != -1) {
				size += read;
			}
			return size;
		}
		finally {
			try {
				is.close();
			}
			catch (IOException ex) {
				debug(() -> "Could not close content-length InputStream for " + getDescription(), ex);
			}
		}
	}

	/**
	 * 返回资源最后的修改时间
	 */
	@Override
	public long lastModified() throws IOException {
		File fileToCheck = getFileForLastModifiedCheck();
		long lastModified = fileToCheck.lastModified();
		if (lastModified == 0L && !fileToCheck.exists()) {
			throw new FileNotFoundException(getDescription() +
					" cannot be resolved in the file system for checking its last-modified timestamp");
		}
		return lastModified;
	}


看到这,如果我们想要扩展Resource ,那么最合适的继承AbstractResource去根据资源的不同覆写方法,而不是直接实现Resource

我们同时看到 getInputStream() 的到处使用,如果是没有实现reset()方法的InputStream,大概是使用一次就不能用了,这也就是为什么Spring强调 每次调用getInputStream() 都会返回一个新的InputStream的原因了

其他子类基本都是根据自身资源类型的不同去实现,感兴趣的朋友可以去仔细看一下

资源加载 ResourceLoader

资源加载器顾名思义用来加载资源的嘛!

public interface ResourceLoader {


	/**
	 * 根据所提供资源的路径 location 返回 Resource 实例,但是它不确保该 Resource 一定存在,需要调用
	 * Resource#exist() 方法来判断。
	 * 默认实现中支持、
	 * 	url 如 file:C/xx
	 *  classPath位置资源 classpath:xxx
	 *  相对路径的资源 WEB-INF/xxx
	 * 如果指定的位置路径没有特定前缀,则会返回适合该特定应用程序上下文 Resource 的类型(这个我们后边会说)
	 */
	Resource getResource(String location);
	
	
	/**
	 * 对于想要获取 ResourceLoader 使用的 ClassLoader 用户来说,可以直接调用该方法来获取。
	 * 在分析 Resource 时,提到了一个类 ClassPathResource ,
	 * 这个类是可以根据指定的 ClassLoader 来加载资源的。
	  */
	ClassLoader getClassLoader();
	
}

继承关系

在这里插入图片描述

DefaultResourceLoader

它是ResourceLoader的默认实现,能接收 ClassLoader 作为构造函数的参数,或者使用不带参数的构造函数。

  • 当无参构造的时候使用的是默认的类加载器
  • 有参构造接收一个ClassLoader
  • 除此以外我们还可以使用setClassLoader(); 去设置类加载器
	/**
	 * Create a new DefaultResourceLoader.
	 * <p>ClassLoader access will happen using the thread context class loader
	 * at the time of actual resource access (since 5.3). For more control, pass
	 * a specific ClassLoader to {@link #DefaultResourceLoader(ClassLoader)}.
	 * @see java.lang.Thread#getContextClassLoader()
	 *
	 * 默认的 ClassLoader
	 */
	public DefaultResourceLoader() {
	}

	/**
	 * Create a new DefaultResourceLoader.
	 * @param classLoader the ClassLoader to load class path resources with, or {@code null}
	 * for using the thread context class loader at the time of actual resource access
	 */
	public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
		this.classLoader = classLoader;
	}


	/**
	 * Specify the ClassLoader to load class path resources with, or {@code null}
	 * for using the thread context class loader at the time of actual resource access.
	 * <p>The default is that ClassLoader access will happen using the thread context
	 * class loader at the time of actual resource access (since 5.3).
	 */
	public void setClassLoader(@Nullable ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	/**
	 * Return the ClassLoader to load class path resources with.
	 * <p>Will get passed to ClassPathResource's constructor for all
	 * ClassPathResource objects created by this resource loader.
	 * @see ClassPathResource
	 */
	@Override
	@Nullable
	public ClassLoader getClassLoader() {
		return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
	}

接下来看一下它的核心实现

/**
	 * 核心实现
	 * @param location
	 * @return {@link Resource}
	 */
	@Override
	public Resource getResource(String location) {
		Assert.notNull(location, "Location must not be null");
		// 通过协议解析器解析资源
		// ProtocolResolver是一个策略接口,可以用于自定义协议解析,
		// 比如spring就有一个 “classpath:”开头的特定协议(但是spring并不是自定义ProtocolResolver实现来完成这个功能的)
		// 允许我们添加自定义的协议解析器
		for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
			Resource resource = protocolResolver.resolve(location, this);
			if (resource != null) {
				return resource;
			}
		}
		// 路径解析。寻思从哪儿来的资源,哪儿解析,默认实现返回 ClassPathContextResource
		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 ,是则返回 FileUrlResource 类型的资源,否则返回 UrlResource 类型的资源
				URL url = ResourceUtils.toURL(location);
				return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
			}
			catch (MalformedURLException ex) {
				//这会真没招没招了,寻思从哪儿来的资源,哪儿解析,要是子类本身就没招就返回ClassPathContextResource
				return getResourceByPath(location);
			}
		}
	}

之前我们说:如果指定的位置路径没有特定前缀,则会返回适合该特定应用程序上下文 Resource的类型 就是由getResourceByPath(location); 去完成的

	
/**
 * 由子类实现实现
 */
protected Resource getResourceByPath(String path) {
		return new ClassPathContextResource(path, getClassLoader());
}

我们现在看一下协议解析器,ProtocolResolver

ProtocolResolver:

用户自定义协议资源解决策略,作为 DefaultResourceLoaderSPI,允许用户自定义资源加载协议,而不需要继承ResourceLoader 的子类。

接口定义:

@FunctionalInterface
public interface ProtocolResolver {
	@Nullable
	Resource resolve(String location, ResourceLoader resourceLoader);

}

比如,我们现在想实现一个资源加载方式,只要是abc:开头的路径,都去classPath下加载


public class CustomProtocolResolver implements ProtocolResolver {



	@Override
	public Resource resolve(String location, ResourceLoader resourceLoader) {

		System.out.println("My custom resolver has started executing");

		if (location.startsWith("abc:")) {
			return resourceLoader.getResource("classpath:" + location.substring("abc:".length()));
		}

		// 如果无法解析或找不到资源,则返回null
		return null;
	}

	public static void main(String[] args) {
		DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();

		defaultResourceLoader.addProtocolResolver(new CustomProtocolResolver());

		Resource resource = defaultResourceLoader.getResource("abc:Spring-config.xml");

		System.out.println("Does the resource exist ? " + resource.exists());
	}
}

执行结果如下:

My custom resolver has started executing
My custom resolver has started executing
Does the resource exist ? true

可以看到是正常加载到了资源! My custom resolver has started executing 为什么我们打印的这段话执行了两边呢?

 // 默认先走我们的自定义的协议解析器
defaultResourceLoader.getResource("abc:Spring-config.xml");
 // 然后又在自定义的协议解析器调用了 
resourceLoader.getResource("classpath:" + location.substring("abc:".length()))

ResourcePatternResolver

如果说DefaultResourceLoader 是默认实现,那么这个接口就是一个扩展接口,定义了将位置模式(例如,Ant 样式路径模式)解析为对象的策略

public interface ResourcePatternResolver extends ResourceLoader {
    
	// 配置Classpath 下所有资源的特殊前缀
	String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
    
	// 为什么说是扩展,因为它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例
	Resource[] getResources(String locationPattern) throws IOException;
    
}
继承关系

在这里插入图片描述

这次了我们先不看ApplicationContext,这家伙逮谁认谁爹,要不然咋说富二代呢!

先看下PathMatchingResourcePatternResolver 最常用的一个子类

PathMatchingResourcePatternResolver

它不仅能够支持 ResourceLoader 里头的加载方式,也支持ResourcePatternResolver 中定义的classpath*:还能支持Ant风格的匹配

比如:

  • * 用于匹配路径中的任何字符(不包括路径分隔符 /)。
  • ** 用于递归匹配路径中的任何字符,包括路径分隔符 /
/**
 * 内置的 ResourceLoader 资源定位器
 */
private final ResourceLoader resourceLoader;
/**
 *  路径匹配器,默认是 ant风格的解析
 */
private PathMatcher pathMatcher = new AntPathMatcher();

public PathMatchingResourcePatternResolver() {
	this.resourceLoader = new DefaultResourceLoader();
}

public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
	Assert.notNull(resourceLoader, "ResourceLoader must not be null");
	this.resourceLoader = resourceLoader;
}

public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
	this.resourceLoader = new DefaultResourceLoader(classLoader);
}
  • 内置了一个资源定位器虽然直接也能实现ResourceLoader但毕竟DefaultResourceLoader都实现的差不多了
  • 路径匹配器:我们也可以去实现PathMatcher接口调用setPathMatcher();去自定义实现我们的路径匹配器
  • PathMatchingResourcePatternResolver 在实例化的时候,可以指定一个 ResourceLoader,如果不指定的话,它会在内部构造一个DefaultResourceLoader

我们知道ResourcePatternResolver作为ResourceLoader 的扩展接口,那必然是要实现getResource();方法的

@Override
public Resource getResource(String location) {
	return getResourceLoader().getResource(location);
}

public ResourceLoader getResourceLoader() {
	return this.resourceLoader;
}

它的实现则来源于内部维护的ResourceLoader,委托给相应的ResourceLoader来实现,我们实例化PathMatchingResourcePatternResolver 时如果未指定ResourceLoader,走的还是DefaultResourceLoadergetResourceLoader() 那一套

getResources

对于扩展的ResourceLoader 扩展的核心实现

	@Override
	public Resource[] getResources(String locationPattern) throws IOException {
		Assert.notNull(locationPattern, "Location pattern must not be null");

		// 如果资源位置模式以 "classpath*:" 开头
		if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
			// 去掉前缀 "classpath*:",获取真实的模式路径
			String locationPatternWithoutPrefix = locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length());

			// 首先搜索模块路径下的资源 
			Set<Resource> resources = findAllModulePathResources(locationPatternWithoutPrefix);

			// 接下来搜索类路径下的资源。路径包含通配符
			if (getPathMatcher().isPattern(locationPatternWithoutPrefix)) {
				// 类路径资源模式,执行路径匹配搜索,(认真的按照通配符匹配)
				Collections.addAll(resources, findPathMatchingResources(locationPattern));
			}
			else {
				// 路径不包含通配符,获取具有给定名称的所有类路径资源,(路径我有了,我一把梭哈了)
				Collections.addAll(resources, findAllClassPathResources(locationPatternWithoutPrefix));
			}

			// 将资源集合转换为数组并返回
			return resources.toArray(new Resource[0]);
		}
		else {
			// 通常只在前缀后查找模式,例如 "war:" 协议的情况下查找 "*/" 分隔符之后的模式
			int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
					locationPattern.indexOf(':') + 1);

			// 如果路径包含通配符
			if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
				// 是文件模式,执行路径匹配搜索(认真的按照通配符匹配)
				return findPathMatchingResources(locationPattern);
			}
			else {
				// 路径不包含通配符,返回一个单一资源,使用给定名称加载,(又不包含classpath*:,又不包含通配符的,统一按照单个算)
				return new Resource[] {getResourceLoader().getResource(locationPattern)};
			}
		}
	}


  • classpath*

    • 支持通配符:认真的按照通配符匹配
    • 不支持通配符:全给加载了
  • classpath*

    • 路径包含通配符:认真的按照通配符匹配
    • 不支持通配符:统一按照单个算

然后,我们一个一个方法看:

findAllModulePathResources():

​ 主要功能是在 Java 9+ 以后的模块化项目中查找符合给定资源位置模式的资源。 只要是模块化项目,甭管是不是通配符模式,它都好使

protected Set<Resource> findAllModulePathResources(String locationPattern) throws IOException {
    // 创建一个 LinkedHashSet 用于存储结果资源集合
    Set<Resource> result = new LinkedHashSet<>(16);

    // 如果应用程序正在运行在原生映像(Native Image)环境中,直接返回空结果
    /*
    Native Image 是一种将 Java 代码提前编译为独立可执行文件(称为原生映像)的技术。
    此可执行文件包括应用程序类、其依赖项中的类、运行时库类以及来自 JDK 的静态链接本机代码。
    它不在 Java VM 上运行,但包含来自不同运行时系统(称为“Substrate VM”)的必要组件,
    如内存管理、线程调度等。Substrate VM 是运行时组件(如去优化器、垃圾收集器、线程调度等)的名称。
    与 JVM 相比,生成的程序具有更快的启动时间和更低的运行时内存开销。感兴趣的同学可以了解一下
    */
    if (NativeDetector.inNativeImage()) {
        return result;
    }

    // 去掉资源位置模式中的前导斜杠:/*.xml转 *.xml
    String resourcePattern = stripLeadingSlash(locationPattern);

    // 创建一个谓词,用于检查资源路径是否与资源位置模式匹配
    Predicate<String> resourcePatternMatches = (getPathMatcher().isPattern(resourcePattern) ?
            path -> getPathMatcher().match(resourcePattern, path) :
            resourcePattern::equals);

    try {
        // 获取启动模块层(ModuleLayer.boot())的配置信息,并对每个非系统模块进行处理
        ModuleLayer.boot().configuration().modules().stream()
                .filter(isNotSystemModule) // 过滤掉系统模块
                .forEach(resolvedModule -> {
                    // NOTE: ModuleReader 和 ModuleReader.list() 返回的 Stream 必须被关闭。
                    try (ModuleReader moduleReader = resolvedModule.reference().open();
                            Stream<String> names = moduleReader.list()) {
                        // 过滤出与资源位置模式匹配的资源路径,并通过模块读取器查找资源
                        names.filter(resourcePatternMatches)
                                .map(name -> findResource(moduleReader, name))
                                .filter(Objects::nonNull)
                                .forEach(result::add);
                    }
                    catch (IOException ex) {
                        // 如果读取模块内容失败,记录日志并抛出 UncheckedIOException
                        if (logger.isDebugEnabled()) {
                            logger.debug("Failed to read contents of module [%s]".formatted(resolvedModule), ex);
                        }
                        throw new UncheckedIOException(ex);
                    }
                });
    }
    catch (UncheckedIOException ex) {
        // 捕获并解包 UncheckedIOException 以符合该方法的异常抛出要求
        throw ex.getCause();
    }

    // 如果日志级别是 TRACE,记录已解析的模块路径资源
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved module-path location pattern [%s] to resources %s".formatted(resourcePattern, result));
    }
    return result;
}

这个在非模块化的项目中其实没有意义。而在模块化项目中会让findPathMatchingResources()findAllClassPathResources()变得没有意义,因为资源已经被他加载完毕了

findPathMatchingResources();

这是非模块化项目中使用的通配符匹配加载资源的方法

	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 rootDirUrl = rootDirResource.getURL();

			// 如果是 Equinox OSGi 环境并且 URL 协议以 "bundle" 开头,则使用 Equinox 方法解析资源
			if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
				URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
				if (resolvedUrl != null) {
					rootDirUrl = resolvedUrl;
				}
				rootDirResource = new UrlResource(rootDirUrl);
			}

			// 根据 URL 协议执行不同的资源匹配方法
			if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
				// 处理 VFS 协议(例如 JBoss VFS)
				result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
			}
			else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
				// 处理 Jar URL 或 Jar 资源
				result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
			}
			else {
				// 处理普通文件系统路径
				result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
			}
		}

		// 如果日志级别是 TRACE,记录已解析的资源
		if (logger.isTraceEnabled()) {
			logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
		}

		// 将结果转换为数组并返回
		return result.toArray(new Resource[0]);
	}

这一段主要是确定下来根目录是什么,然后迭代获取资源

然后我们再来看下获得根目录的方法

determineRootDir()
// 确定资源位置中的根目录
protected String determineRootDir(String location) {
    // 找到资源位置中协议部分的末尾位置
    int prefixEnd = location.indexOf(':') + 1;
    
    // 初始化根目录的末尾位置为资源位置的末尾
    int rootDirEnd = location.length();
    
    // 循环检查资源位置中的通配符部分
    while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
        // 如果通配符部分存在,将根目录的末尾位置设为通配符前的最后一个斜杠的位置
        rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
    }
    
    // 如果没有斜杠,将根目录的末尾位置设为协议部分的末尾
    if (rootDirEnd == 0) {
        rootDirEnd = prefixEnd;
    }
    
    // 返回确定的根目录部分
    return location.substring(0, rootDirEnd);
}

假设现在有这么一个路径:classpath*:com/example/**/*.xml

处理完毕我们会得到 classpath*:com/example

不同 URL 协议的资源匹配
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
    // 处理 VFS 协议(例如 JBoss VFS)
    result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
    // 处理 Jar URL 或 Jar 资源
    result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
    // 处理普通文件系统路径
    result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}

这部分逻辑根据资源的 URL 协议执行不同的资源匹配方法:

  • 如果资源的 URL 协议以 ResourceUtils.URL_PROTOCOL_VFS 开头,那么它可能是使用 VFS(Virtual File System)协议表示的资源,例如在 JBoss 环境中。在这种情况下,使用 VfsResourceMatchingDelegate 类来查找匹配的资源。
  • 如果资源的 URL 协议被识别为 Jar URL(例如 jar:zip:),或者资源被确定为 Jar 资源(使用 isJarResource 方法判断),则认为它是 Jar 文件中的资源。在这种情况下,使用 doFindPathMatchingJarResources 方法来查找匹配的 Jar 资源。
  • 如果上述两种情况都不匹配,那么默认情况下认为资源是普通的文件系统路径,然后使用 doFindPathMatchingFileResources 方法来查找匹配的文件系统资源。

展开来说,代码量比较大,后期我们专门开一篇文章详细介绍

我们在来看下以 "classpath*:" 开头但是不包含通配符的方法匹配(非模块化项目)

findAllClassPathResources()
protected Resource[] findAllClassPathResources(String location) throws IOException {
    // 去除路径字符串中可能存在的前导斜杠'/'
    String path = stripLeadingSlash(location);
    
    // 调用特定方法查找所有位于给定类路径位置的资源
    Set<Resource> result = doFindAllClassPathResources(path);
    
    // 如果启用了日志的跟踪级别,记录已解析的类路径位置和找到的资源
    if (logger.isTraceEnabled()) {
        logger.trace("已解析类路径位置 [" + path + "],找到的资源为:" + result);
    }
    
    // 将资源集合转换为资源数组并返回
    return result.toArray(new Resource[0]);
}

主要的逻辑执行就是doFindAllClassPathResources();

doFindAllClassPathResources()
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    // 创建一个存储资源的有序集合(LinkedHashSet)
    Set<Resource> result = new LinkedHashSet<>(16);

    // 获取当前类的类加载器
    ClassLoader cl = getClassLoader();

    // 使用类加载器获取所有位于给定路径的资源的URL枚举
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));

    // 遍历资源URL枚举
    while (resourceUrls.hasMoreElements()) {
        // 获取下一个资源的URL
        URL url = resourceUrls.nextElement();

        // 将URL转换为Resource对象并添加到结果集合中
        result.add(convertClassLoaderURL(url));
    }

    // 如果路径为空,通常表示查找根类路径,这可能不足够
    if (!StringUtils.hasLength(path)) {
        // 上面的结果可能不完整,可能只包含文件系统引用。
        // 我们还需要指向类路径上每个JAR文件的指针添加到result
        addAllClassLoaderJarRoots(cl, result);
    }

    // 返回包含资源的集合
    return result;
}
  • ClassLoader如果在PathMatchingResourcePatternResolver 实列化的时候没有传入,则是使用的ClassLoader.getSystemResources(path)
    // 是不是熟悉的双亲委派机制呢?
	public Enumeration<URL> getResources(String name) throws IOException {
        Objects.requireNonNull(name);
        @SuppressWarnings("unchecked")
        Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
        if (parent != null) {
            tmp[0] = parent.getResources(name);
        } else {
            tmp[0] = BootLoader.findResources(name);
        }
        tmp[1] = findResources(name);

        return new CompoundEnumeration<>(tmp);
    }

addAllClassLoaderJarRoots(cl, result);

这个方法呢主要是把jar文件的指针添加资源的集合当中,感兴趣的朋友可以看一下

总结:

在这里插入图片描述

  • Spring框架为了更好地处理资源及其定位问题,引入了ResourceResourceLoader的抽象概念
  • AbstractResource,是Resource接口的默认实现。通过继承这个类,我们可以更轻松地创建自定义的资源类型。我们只需要覆盖必要的方法即可
  • DefaultResourceLoaderResourceLoader接口的默认实现。如果需要自定义ResourceLoader,可以继承这个类。此外,我们还可以通过实现ProtocolResolver接口来定义自己的资源加载协议。
  • 为了扩展ResourceLoader 每次只能返回单个资源,引入了ResourcePatternResolver接口
  • PathMatchingResourcePatternResolver允许根据指定的通配符模式(locationPattern)返回多个资源,并且增加了 Java 9+ 以后的模块化项目的支持
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值