ResourceLoader 接口是用来加载 Resource 对象的,换句话说,就是当一个对象需要获取 Resource 实例时,可以选择实现 ResourceLoader 接口。
public interface ResourceLoader { /** Pseudo URL prefix for loading from the class path: "classpath:" */ String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; Resource getResource(String location); @Nullable ClassLoader getClassLoader(); }
当你在指定应用上下文调用 getResource() 方法时,而指定的位置路径又没有包含特定的前缀,spring 会根据当前应用上下文来决定返回哪一种类型 Resource。举个例子,假设下面的代码片段是通过 ClassPathXmlApplicationContext 实例来调用的,
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
那 spring 会返回一个 ClassPathResource 对象;类似的,如果是通过实例 FileSystemXmlApplicationContext 实例调用的,返回的是一个 FileSystemResource 对象;如果是通过 WebApplicationContext 实例的,返回的是一个 ServletContextResource 对象…… 如上所说,你就可以在指定的应用上下中使用 Resource 实例来加载当前应用上下文的资源。
还有另外一种场景里,如在其他应用上下文里,你可能会强制需要获取一个 ClassPathResource 对象,这个时候,你可以通过加上指定的前缀来实现这一需求,如:
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
类似的,你可以通过其他任意的 url 前缀来强制获取 UrlResource 对象:
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt");
下面,给出一个表格来总结一下 spring 根据各种位置路径加载资源的策略:
Prefix | Example | Explanation |
---|---|---|
classpath: |
|
Loaded from the classpath. |
file: |
Loaded as a |
|
http: |
Loaded as a |
|
(none) |
|
Depends on the underlying |
ResourcePatternResolver继承ResourceLoader,增加了一个方法通过一个path可以返回多个Resource:
public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; Resource[] getResources(String locationPattern) throws IOException; }
- 以ResourceLoader结尾的类实现了ResourceLoader接口,Spring给出的默认实现是DefaultResourceLoader。
- 以ResourcePatternResolver结尾的类实现了ResourcePatternResolver接口,Spring给出了两个实现类PathMatchingResourcePatternResolver和ServletContextResourcePatternResolver。
- 以ApplicationContext结尾的类也实现了ResourcePatternResolver接口,最上层的实现类是AbstractApplicationContext,继承了DefaultResourceLoader并默认持有一个PathMatchingResourcePatternResolver的引用。
下面分析一下DefaultResourceLoader和PathMatchingResourcePatternResolver。
public class DefaultResourceLoader implements ResourceLoader { @Nullable private ClassLoader classLoader; private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4); private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap<>(4); public DefaultResourceLoader() { this.classLoader = ClassUtils.getDefaultClassLoader(); } public DefaultResourceLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; } public void setClassLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; } @Override @Nullable public ClassLoader getClassLoader() { return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader()); } public void addProtocolResolver(ProtocolResolver resolver) { Assert.notNull(resolver, "ProtocolResolver must not be null"); this.protocolResolvers.add(resolver); } public Collection<ProtocolResolver> getProtocolResolvers() { return this.protocolResolvers; } @SuppressWarnings("unchecked") public <T> Map<Resource, T> getResourceCache(Class<T> valueType) { return (Map<Resource, T>) this.resourceCaches.computeIfAbsent(valueType, key -> new ConcurrentHashMap<>()); } public void clearResourceCaches() { this.resourceCaches.clear(); } @Override public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); for (ProtocolResolver protocolResolver : this.protocolResolvers) { Resource resource = protocolResolver.resolve(location, this); if (resource != null) { return resource; } } if (location.startsWith("/")) { return getResourceByPath(location); } else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { // Try to parse the location as a URL... URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // No URL -> resolve as resource path. return getResourceByPath(location); } } } protected Resource getResourceByPath(String path) { return new ClassPathContextResource(path, getClassLoader()); } protected static class ClassPathContextResource extends ClassPathResource implements ContextResource { public ClassPathContextResource(String path, @Nullable ClassLoader classLoader) { super(path, classLoader); } @Override public String getPathWithinContext() { return getPath(); } @Override public Resource createRelative(String relativePath) { String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath); return new ClassPathContextResource(pathToUse, getClassLoader()); } } }
DefaultResourceLoader的getResource()方法中,首先会循环protocolResolvers,直到碰到一个能处理此location的ProtocolResolver,返回其非null的经过resolver处理返回的Resource对象。Spring4.3给拱了一个方法:
addProtocolResolver()来加入我们自定义的ProtocalResolver来改变DefaultResourceLoader默认的行为。
其默认根据location的格式来返回不同的Resource实例。
- 以/开头返回一个ClassPathContextResource对象,继承于ClassPathResource,增加了一个方法getPathWithinContext()。
- 以classpath:开头的返回new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader())。
- 否则尝试以location构造URL对象,如果是file|vfsfile|vfs开头的返回FileUrlResource否则返回UrlResource
- 构造URL失败,还是以ClassPathContextResource返回。
PathMatchingResourcePatternResolver内有一个很重要的成员变量pathMatcher,是一个AntPathMatcher实例,对参数的解析主要用到它,先看看它的简单介绍:
下面主要看一下PathMatchingResourcePatternResolver的getResources()方法。
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver { private final ResourceLoader resourceLoader; 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); } @Override public Resource getResource(String location) { return getResourceLoader().getResource(location); } @Override public Resource[] getResources(String locationPattern) throws IOException { Assert.notNull(locationPattern, "Location pattern must not be null"); if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { // a class path resource (multiple resources for same name possible) if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { // a class path resource pattern return findPathMatchingResources(locationPattern); } else { // all class path resources with the given name return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } } else { // Generally only look for a pattern after a prefix here, // and on Tomcat only after the "*/" separator for its "war:" protocol. int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(":") + 1); if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { // a file pattern return findPathMatchingResources(locationPattern); } else { // a single resource with the given name return new Resource[] {getResourceLoader().getResource(locationPattern)}; } } } }
以classpath*:开头并且路径其余部分还包含*或?则调用findPathMatchingResources()方法,找出所有classpath下可以匹配locationPattern的Resource。
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(); if (equinoxResolveMethod != null) { if (rootDirUrl.getProtocol().startsWith("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)) { result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher())); } else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) { result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern)); } else { result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); } } if (logger.isDebugEnabled()) { logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result); } return result.toArray(new Resource[result.size()]); }
rootDirPath为包含*或?上一级目录,例如locationPattern="classpath*:org/springframework/core/io/sup*/*.class",则rootDirPath="classpath*:org/springframework/core/io/",subPattern="sup*/*.class",接下来调用getResources()方法,这次getResources的入参是不带非前缀通配符的了,所以会走findAllClassPathResources()方法。
protected Resource[] findAllClassPathResources(String location) throws IOException { String path = location; if (path.startsWith("/")) { path = path.substring(1); } Set<Resource> result = doFindAllClassPathResources(path); if (logger.isDebugEnabled()) { logger.debug("Resolved classpath location [" + location + "] to resources " + result); } return result.toArray(new Resource[result.size()]); }
如果以“/”开头会去掉“/”,因为doFindAllClassPathResources()方法内是调用ClassLoader的getResources方法获取指定的URL,这里有必要提一句,ClassLoader的getResources()方法会返回所有classpath下的URL,就是如果有多个jar(也包含当前工程)包含有path指定的资源路径,都会找出来包装为URLResource返回。
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException { 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)) { // 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; }
然后做三个判断,对不同协议的rootDirUrl分别调用不同的方法来找到匹配的资源。
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher())); } else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) { result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern)); } else { result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); }
我们看一下非vfs协议和非jar中的路径情况是如何匹配到subPattern的资源的。
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { File rootDir; try { rootDir = rootDirResource.getFile().getAbsoluteFile(); } catch (IOException ex) { if (logger.isWarnEnabled()) { logger.warn("Cannot search for matching files underneath " + rootDirResource + " because it does not correspond to a directory in the file system", ex); } return Collections.emptySet(); } return doFindMatchingFileSystemResources(rootDir, subPattern); }
首先获取到rootDirResource的绝对路径为了可以使用File定位到具体文件,调用doFindMatchingFileSystemResources()方法
protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]"); } Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern); Set<Resource> result = new LinkedHashSet<>(matchingFiles.size()); for (File file : matchingFiles) { result.add(new FileSystemResource(file)); } return result; }
retrieveMatchingFiles方法找出rootDir下能与subPattern匹配的文件,将File包装成FileSystemResource类型返回。看一下具体找文件的过程:
protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException { if (!rootDir.exists()) { // Silently skip non-existing directories. if (logger.isDebugEnabled()) { logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist"); } return Collections.emptySet(); } if (!rootDir.isDirectory()) { // Complain louder if it exists but is no directory. if (logger.isWarnEnabled()) { logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory"); } return Collections.emptySet(); } if (!rootDir.canRead()) { if (logger.isWarnEnabled()) { logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() + "] because the application is not allowed to read the directory"); } 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; }其中将路径分隔符统一为“/”,避免windows系统下路径为\\导致AntPathMatcher不能处理,然后调用doRetrieveMatchingFiles()方法,
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Searching directory [" + dir.getAbsolutePath() + "] for files matching pattern [" + fullPattern + "]"); } File[] dirContents = dir.listFiles(); if (dirContents == null) { if (logger.isWarnEnabled()) { logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]"); } return; } Arrays.sort(dirContents); for (File content : dirContents) { 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); } } }遍历dir下所有的文件或文件夹,如果是文件则调用AntPathMatcher的match()方法判断当前文件的绝对路径是否满足fullPattern的匹配规则,满足则加入result里保存下来。如果是文件夹,则递归调用doRetrieveMatchingFiles()方法,超出此文件夹下满足上述条件的文件。