spring 中的资源文件加载

spring 中定义了加载资源(例如,类路径或文件系统资源)的策略接口 ResourceLoader 和 扩展接口 ResourcePatternResolver。并通过 ApplicationContext 来提供资源文件加载功能。

功能提供 AbstractApplicationContext

ApplicationContext  的顶级抽象实现类为 AbstractApplicationContext,创建应用时,会在构造方法中,创建一个 ResourcePatternResolver 实例对象,作为资源文件加载路径匹配的解析器。

public AbstractApplicationContext() {
	this.resourcePatternResolver = getResourcePatternResolver();
}

public AbstractApplicationContext(@Nullable ApplicationContext parent) {
	this();
	setParent(parent);
}

并且在应用中,如果没有新建指定,则整个应用中采用的资源文件解析器都是在此时创建的 resourcePatternResolver。

protected ResourcePatternResolver getResourcePatternResolver() {
	return new PathMatchingResourcePatternResolver(this);
}

public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
	this.resourceLoader = resourceLoader;
}

 传入 AbstractApplicationContext 子类对象作为 ResourceLoader,创建一个 PathMatchingResourcePatternResolver 实例对象。

通过类关系可以看到,AbstractApplicationContext 通过继承 DefaultResourceLoader,实现了 ResourceLoader 接口。它本身只实现了定义在 ResourcePatternResolver 中的接口方法。

@Override
public Resource[] getResources(String locationPattern) throws IOException {
	return this.resourcePatternResolver.getResources(locationPattern);
}

可以看到,其实是委托给了 AbstractApplicationContext 实例化时创建的 PathMatchingResourcePatternResolver 来实现获取资源文件。

下面我们来看看 spring 中通过操作资源文件加载的这几个类,到底是如何实现获取给定路径下的资源文件的。

ResourceLoader

这个接口定义了两个抽象方法:

Resource getResource(String location);

ClassLoader getClassLoader();

这两个方法的实现在 DefaultResourceLoader 中,下面来看下具体实现:

@Override
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;
		}
	}

	if (location.startsWith("/")) {
		return getResourceByPath(location);
	}
	// 以 "classpath:" 开头
	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);
			return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
		}
		catch (MalformedURLException ex) {
			// 不是 URL,以资源路径解析
			return getResourceByPath(location);
		}
	}
}

// 创建 ClassPathContextResource
protected Resource getResourceByPath(String path) {
	return new ClassPathContextResource(path, getClassLoader());
}
// 获取 ClassLoader,默认采用线程上下文类加载器
@Override
@Nullable
public ClassLoader getClassLoader() {
	return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}

@Nullable
public static ClassLoader getDefaultClassLoader() {
	ClassLoader cl = null;
	try {
		cl = Thread.currentThread().getContextClassLoader();
	}
	catch (Throwable ex) {
		...
	}
	if (cl == null) {
		// No thread context class loader -> use class loader of this class.
		// 没有线程上下文类加载器,采用当前 Class 对应的 ClassLoader 
		cl = ClassUtils.class.getClassLoader();
		if (cl == null) {
			// getClassLoader() 返回 null 表明采用 Bootstrap ClassLoader,即启动类加载器
			// 此时获取 应用程序类加载器
			try {
				cl = ClassLoader.getSystemClassLoader();
			}
			catch (Throwable ex) {
				...
			}
		}
	}
	return cl;
}

此处要注意的一点,spring 默认采用线程上下文类加载器来加载资源文件。这个类加载器可以通过Thread#setContextClassLoader 进行设置,如果未设置,默认从父线程中继承,如果整个应用程序中都没有设置,默认就是应用程序类加载器。

ResourcePatternResolver

继承 ResourceLoader,扩展了对路径中通配符的解析。

Resource[] getResources(String locationPattern) throws IOException;

这个方法的实现在 PathMatchingResourcePatternResolver,下面来看看是如何实现的:

@Override
public Resource[] getResources(String locationPattern) throws IOException {
	// 以 classpath*: 开头
	if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
		// 截取 classpath*: 之后的路径,通过 AntPathMatcher 进行匹配
		if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
			// 匹配到了,采用匹配模式获取类资源
			return findPathMatchingResources(locationPattern);
		}
		else {
			// 未匹配到,采用给定的路径名称下的所有类资源
			return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
		}
	}
	else {
		// 非 classpath*: 开头
		// war: 开头,取路径中 "*/" 之后的部分匹配
		// 非 war: 开头,取路径中 ":" 之后的部分匹配
		int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
				locationPattern.indexOf(':') + 1);
		if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
			// a file pattern
			return findPathMatchingResources(locationPattern);
		}
		else {
			// 给定名称的单个资源
			return new Resource[] {getResourceLoader().getResource(locationPattern)};
		}
	}
}

路径匹配模式下的资源文件获取,都是在 PathMatchingResourcePatternResolver 中实现的,其中路径中通配符的匹配又委托给 AntPathMatcher 来实现。

实现类 PathMatchingResourcePatternResolver

在实现类中,以路径是否以 "classpath*:" 开头分为两个分支。

// AntPathMatcher
// 判断给定的 path 中是否存在 '*' 或 '?',以及成对的 "{}"
@Override
public boolean isPattern(@Nullable String path) {
	if (path == null) {
		return false;
	}
	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;
			continue;
		}
		if (c == '}' && uriVar) {
			return true;
		}
	}
	return false;
}

AntPathMatcher 中的 isPattern 逻辑非常简单,就是判断是否存在通配符 '*'、'?',以及成对出现的"{}"。

匹配到了通配符,调用 findPathMatchingResources。

// PathMatchingResourcePatternResolver
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {

	// rootDirPath =  classpath*:com/icheetor/annotation/service/
	String rootDirPath = determineRootDir(locationPattern);
	
	// 子路径匹配模型
	// subPattern = **/*.class
	String subPattern = locationPattern.substring(rootDirPath.length());
	// 获取根路径下所有资源,此时路径不含通配符
	// 再次进入 PathMatchingResourcePatternResolver#getResources,这次执行 findAllClassPathResources
	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 war zip vfszip wsjar
		else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
			result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
		}
		else {
			result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
		}
	}
	
	return result.toArray(new Resource[0]);
}

实际应用中,配置的包扫描路径,当传递到调用此方法时,经过解析拼接,locationPattern 是一个包含了默认资源匹配的字符串:

classpath*:com/icheetor/annotation/service/**/*.class

所以上面方法的实现逻辑:

  1. 获取 locationPattern 中不包含通配符的最长路径,作为根路径
  2. 截取子路径匹配模型 subPattern
  3. 获取根路径下所有的资源文件
  4. 遍历获取到的所有资源文件,和 subPattern 进行匹配,将匹配到的放入结果集中。

下面我们一步步看下这个方法的实现。

protected String determineRootDir(String location) {
	int prefixEnd = location.indexOf(':') + 1;
	int rootDirEnd = location.length();
	// 截取路径进行匹配,直到给定路径不满足 AntPathMatcher 匹配规则
	// com/icheetor/annotation/service/**/*.class
	// com/icheetor/annotation/service/**/
	// com/icheetor/annotation/service/
	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);
}

 对 ":" 之后的部分,不断循环,从后向前推进,最终获取不含通配符的路径作为根目录。举个例子:rootDirPath = classpath*:com/icheetor/annotation/service/

待获取到 rootDirPath 之后,再次调用 PathMatchingResourcePatternResolver#getResources,获取根路径下所有资源文件,此时不存在通配符,执行 findAllClassPathResources。

protected Resource[] findAllClassPathResources(String location) throws IOException {
	String path = location;
	if (path.startsWith("/")) {
		path = path.substring(1);
	}
	Set<Resource> result = doFindAllClassPathResources(path);
	
	return result.toArray(new Resource[0]);
}
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
	Set<Resource> result = new LinkedHashSet<>(16);
	// 通过创建 PathMatchingResourcePatternResolver 时传入的 ResourceLoader 来获取 ClassLoader
	ClassLoader cl = getClassLoader();
	Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
	while (resourceUrls.hasMoreElements()) {
		URL url = resourceUrls.nextElement();
		// 创建 UrlResource,添加进 result
		result.add(convertClassLoaderURL(url));
	}
	if (!StringUtils.hasLength(path)) {
		// The above result is likely to be incomplete, i.e. only containing file system references.
		// We need to have pointers to each of the jar files on the classpath as well...
		addAllClassLoaderJarRoots(cl, result);
	}
	return result;
}
@Override
@Nullable
public ClassLoader getClassLoader() {
	return getResourceLoader().getClassLoader();
}

AbstractApplicationContext 继承 DefaultResourceLoader,此时调用 getResourceLoader() 获取到的其实是 DefaultResourceLoader,调用 getClassLoader() 获取线程上下文类加载器,之后通过类加载器获取资源文件

将解析出的 URL 包装为 org.springframework.core.io.UrlResource。一个路径对应一个 UrlResource,此时只有一个 UrlResource,返回作为 rootDirResources。接着遍历 rootDirResources,获取到 URL,根据不同协议调用不同的处理方法,为 "file:/..." 调用doFindPathMatchingFileResources。

protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
		throws IOException {

	File rootDir;
	try {
		// 获取绝对路径
		rootDir = rootDirResource.getFile().getAbsoluteFile();
	}
	catch (FileNotFoundException ex) {
		...
		return Collections.emptySet();
	}
	catch (Exception ex) {
		...
		return Collections.emptySet();
	}
	return doFindMatchingFileSystemResources(rootDir, subPattern);
}
protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
	...
	Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
	Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
	for (File file : matchingFiles) {
		result.add(new FileSystemResource(file));
	}
	return result;
}

protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
	// 不存在
	if (!rootDir.exists()) {
		return Collections.emptySet();
	}
	// 不是文件夹
	if (!rootDir.isDirectory()) {
		return Collections.emptySet();
	}
	// 不可读
	if (!rootDir.canRead()) {
		return Collections.emptySet();
	}
	String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
	if (!pattern.startsWith("/")) {
		fullPattern += "/";
	}
	// 拼接出完整的路径匹配模式
	fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
	Set<File> result = new LinkedHashSet<>(8);
	doRetrieveMatchingFiles(fullPattern, rootDir, result);
	return result;
}
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {

	for (File content : listDirectory(dir)) {
		String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
		// content 是文件夹
		if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
			// 不可读
			if (!content.canRead()) {
				...
			}
			else {
				// 对 content 进行递归调用
				doRetrieveMatchingFiles(fullPattern, content, result);
			}
		}
		// 
		if (getPathMatcher().match(fullPattern, currPath)) {
			result.add(content);
		}
	}
}

获取根目录绝对路径,与 subPattern 拼接出 fullPattern,为匹配做好准备,接着获取 rootDir 下的所有文件,遍历,遇见文件夹递归检索,接着将匹配到的文件加入结果集。

文件路径匹配采用 AntPathMatcher 实现,在创建 PathMatchingResourcePatternResolver时,默认创建了 AntPathMatcher 实例对象,作为 PathMatchingResourcePatternResolver 中的一个字段。具体的匹配过程请参考 spring 中的路径匹配

接着就得到了匹配的资源文件,matchingFiles,遍历 matchingFiles,封装 FileSystemResource,添加进结果集 result。

至此,就获得了配置包路径下符合条件的资源,即字节码文件,后面实现不同功能时,再对这些 class 文件进行操作即可。

我们来总结一下,通过 ClassLoader 加载配置路径,得到 URL,将其封装为 URLResource,根据 URL 中不同协议,调用不同处理方法,默认 file 协议,根据 URL 得到一个 File,接着就是遍历 File 下子文件,获取匹配的 file,封装成 FileSystemResource 返回。

伪代码如下:

public void test() {
	ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
	try {
		Enumeration<URL> resources = contextClassLoader.getResources("com/icheetor/service");
		while (resources.hasMoreElements()) {
			URL url = resources.nextElement();
			File rootDirFile = new File(url.getPath());

			String full_pattern = rootDirFile.getAbsolutePath() + sub_pattern;

			for (File content : rootDirFile.listFiles()) {
				String currentPath = content.getAbsolutePath();
				if (content.isDirectory()) {
				   // 递归
				}
				if (matcher.match(full_pattern, content)) {
					// 封装 FileSystemResource,添加进结果集
				}
			}
		}
	} catch (IOException e) {
		e.printStackTrace();
	}
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潭影空人心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值