原文出自:http://cmsblogs.comjava
在學 Java SE 的時候咱們學習了一個標準類 java.net.URL
,該類在 Java SE 中的定位爲統一資源定位器(Uniform Resource Locator),可是咱們知道它的實現基本只限於網絡形式發佈的資源的查找和定位。然而,實際上資源的定義比較普遍,除了網絡形式的資源,還有以二進制形式存在的、以文件形式存在的、以字節流形式存在的等等。並且它能夠存在於任何場所,好比網絡、文件系統、應用程序中。因此 java.net.URL
的侷限性迫使 Spring 必須實現本身的資源加載策略,該資源加載策略須要知足以下要求:spring
org.springframework.core.io.Resource
爲 Spring 框架全部資源的抽象和訪問接口,它繼承 org.springframework.core.io.InputStreamSource
接口。做爲全部資源的統一抽象,Source 定義了一些通用的方法,由子類 AbstractResource
提供統一的默認實現。定義以下:數組
public interface Resource extends InputStreamSource { /** * 資源是否存在 */ boolean exists(); /** * 資源是否可讀 */ default boolean isReadable() { return true; } /** * 資源所表明的句柄是否被一個stream打開了 */ default boolean isOpen() { return false; } /** * 是否爲 File */ default boolean isFile() { return false; } /** * 返回資源的URL的句柄 */ URL getURL() throws IOException; /** * 返回資源的URI的句柄 */ URI getURI() throws IOException; /** * 返回資源的File的句柄 */ File getFile() throws IOException; /** * 返回 ReadableByteChannel */ default ReadableByteChannel readableChannel() throws IOException { return Channels.newChannel(getInputStream()); } /** * 資源內容的長度 */ long contentLength() throws IOException; /** * 資源最後的修改時間 */ long lastModified() throws IOException; /** * 根據資源的相對路徑建立新資源 */ Resource createRelative(String relativePath) throws IOException; /** * 資源的文件名 */ @Nullable String getFilename(); /** * 資源的描述 */ String getDescription(); }
類結構圖以下:網絡
從上圖能夠看到,Resource 根據資源的不一樣類型提供不一樣的具體實現,以下:框架
java.io.File
類型資源的封裝,只要是跟 File 打交道的,基本上與 FileSystemResource 也能夠打交道。支持文件和 URL 的形式,實現 WritableResource 接口,且從 Spring Framework 5.0 開始,FileSystemResource 使用NIO.2 API進行讀/寫交互java.net.URL
類型資源的封裝。內部委派 URL 進行具體的資源操做。AbstractResource 爲 Resource 接口的默認實現,它實現了 Resource 接口的大部分的公共實現,做爲 Resource 接口中的重中之重,其定義以下:ide
public abstract class AbstractResource implements Resource { /** * 判斷文件是否存在,若判斷過程產生異常(由於會調用SecurityManager來判斷),就關閉對應的流 */ @Override public boolean exists() { try { return getFile().exists(); } catch (IOException ex) { // Fall back to stream existence: can we open the stream? try { InputStream is = getInputStream(); is.close(); return true; } catch (Throwable isEx) { return false; } } } /** * 直接返回true,表示可讀 */ @Override public boolean isReadable() { return true; } /** * 直接返回 false,表示未被打開 */ @Override public boolean isOpen() { return false; } /** * 直接返回false,表示不爲 File */ @Override public boolean isFile() { return false; } /** * 拋出 FileNotFoundException 異常,交給子類實現 */ @Override public URL getURL() throws IOException { throw new FileNotFoundException(getDescription() + " cannot be resolved to URL"); } /** * 基於 getURL() 返回的 URL 構建 URI */ @Override public URI getURI() throws IOException { URL url = getURL(); try { return ResourceUtils.toURI(url); } catch (URISyntaxException ex) { throw new NestedIOException("Invalid URI [" + url + "]", ex); } } /** * 拋出 FileNotFoundException 異常,交給子類實現 */ @Override public File getFile() throws IOException { throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path"); } /** * 根據 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[255]; int read; while ((read = is.read(buf)) != -1) { size += read; } return size; } finally { try { is.close(); } catch (IOException ex) { } } } /** * 返回資源最後的修改時間 */ @Override public long lastModified() throws IOException { long lastModified = getFileForLastModifiedCheck().lastModified(); if (lastModified == 0L) { throw new FileNotFoundException(getDescription() + " cannot be resolved in the file system for resolving its last-modified timestamp"); } return lastModified; } protected File getFileForLastModifiedCheck() throws IOException { return getFile(); } /** * 交給子類實現 */ @Override public Resource createRelative(String relativePath) throws IOException { throw new FileNotFoundException("Cannot create a relative resource for " + getDescription()); } /** * 獲取資源名稱,默認返回 null */ @Override @Nullable public String getFilename() { return null; } /** * 返回資源的描述 */ @Override public String toString() { return getDescription(); } @Override public boolean equals(Object obj) { return (obj == this || (obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription()))); } @Override public int hashCode() { return getDescription().hashCode(); } }
若是咱們想要實現自定義的 Resource,記住不要實現 Resource 接口,而應該繼承 AbstractResource 抽象類,而後根據當前的具體資源特性覆蓋相應的方法便可。函數
一開始就說了 Spring 將資源的定義和資源的加載區分開了,Resource 定義了統一的資源,那資源的加載則由 ResourceLoader 來統必定義。學習
org.springframework.core.io.ResourceLoader
爲 Spring 資源加載的統一抽象,具體的資源加載則由相應的實現類來完成,因此咱們能夠將 ResourceLoader 稱做爲統一資源定位器。其定義以下:ui
public interface ResourceLoader { String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; Resource getResource(String location); ClassLoader getClassLoader(); }
ResourceLoader 接口提供兩個方法:getResource()
、getClassLoader()
。this
getResource()
根據所提供資源的路徑 location 返回 Resource 實例,可是它不確保該 Resource 必定存在,須要調用 Resource.exist()
方法判斷。該方法支持如下模式的資源加載:
該方法的主要實現是在其子類 DefaultResourceLoader 中實現,具體過程咱們在分析 DefaultResourceLoader 時作詳細說明。
getClassLoader()
返回 ClassLoader 實例,對於想要獲取 ResourceLoader 使用的 ClassLoader 用戶來講,能夠直接調用該方法來獲取,
對於想要獲取 ResourceLoader 使用的 ClassLoader 用戶來講,能夠直接調用 getClassLoader()
方法得到。在分析 Resource 時,提到了一個類 ClassPathResource ,這個類是能夠根據指定的 ClassLoader 來加載資源的。
做爲 Spring 統一的資源加載器,它提供了統一的抽象,具體的實現則由相應的子類來負責實現,其類的類結構圖以下:
與 DefaultResource 類似,DefaultResourceLoader 是 ResourceLoader 的默認實現,它接收 ClassLoader 做爲構造函數的參數或者使用不帶參數的構造函數,在使用不帶參數的構造函數時,使用的 ClassLoader 爲默認的 ClassLoader(通常爲Thread.currentThread().getContextClassLoader()
),能夠經過 ClassUtils.getDefaultClassLoader()
獲取。固然也能夠調用 setClassLoader()
方法進行後續設置。以下:
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()); }
ResourceLoader 中最核心的方法爲 getResource()
,它根據提供的 location 返回相應的 Resource,而 DefaultResourceLoader 對該方法提供了核心實現(它的兩個子類都沒有提供覆蓋該方法,因此能夠判定ResourceLoader 的資源加載策略就封裝 DefaultResourceLoader中),以下:
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); } } }
首先經過 ProtocolResolver 來加載資源,成功返回 Resource,不然調用以下邏輯:
getResourceByPath()
構造 ClassPathContextResource 類型資源並返回。getClassLoader()
獲取當前的 ClassLoader。getResourceByPath()
實現資源定位加載。ProtocolResolver ,用戶自定義協議資源解決策略,做爲 DefaultResourceLoader 的 SPI,它容許用戶自定義資源加載協議,而不須要繼承 ResourceLoader 的子類。在介紹 Resource 時,提到若是要實現自定義 Resource,咱們只須要繼承 DefaultResource 便可,可是有了 ProtocolResolver 後,咱們不須要直接繼承 DefaultResourceLoader,改成實現 ProtocolResolver 接口也能夠實現自定義的 ResourceLoader。
ProtocolResolver 接口,僅有一個方法 Resource resolve(String location, ResourceLoader resourceLoader)
,該方法接收兩個參數:資源路徑location,指定的加載器 ResourceLoader,返回爲相應的 Resource 。在 Spring 中你會發現該接口並無實現類,它須要用戶自定義,自定義的 Resolver 如何加入 Spring 體系呢?調用 DefaultResourceLoader.addProtocolResolver()
便可,以下:
public void addProtocolResolver(ProtocolResolver resolver) { Assert.notNull(resolver, "ProtocolResolver must not be null"); this.protocolResolvers.add(resolver); }
下面示例是演示 DefaultResourceLoader 加載資源的具體策略,代碼以下(該示例參考《Spring 解密》 P89):
ResourceLoader resourceLoader = new DefaultResourceLoader(); Resource fileResource1 = resourceLoader.getResource("D:/Users/chenming673/Documents/spark.txt"); System.out.println("fileResource1 is FileSystemResource:" + (fileResource1 instanceof FileSystemResource)); Resource fileResource2 = resourceLoader.getResource("/Users/chenming673/Documents/spark.txt"); System.out.println("fileResource2 is ClassPathResource:" + (fileResource2 instanceof ClassPathResource)); Resource urlResource1 = resourceLoader.getResource("file:/Users/chenming673/Documents/spark.txt"); System.out.println("urlResource1 is UrlResource:" + (urlResource1 instanceof UrlResource)); Resource urlResource2 = resourceLoader.getResource("http://www.baidu.com"); System.out.println("urlResource1 is urlResource:" + (urlResource2 instanceof UrlResource));
運行結果:
fileResource1 is FileSystemResource:false fileResource2 is ClassPathResource:true urlResource1 is UrlResource:true urlResource1 is urlResource:true
其實對於 fileResource1 咱們更加但願是 FileSystemResource 資源類型,可是事與願違,它是 ClassPathResource 類型。在getResource()
資源加載策略中,咱們知道 D:/Users/chenming673/Documents/spark.txt
資源其實在該方法中沒有相應的資源類型,那麼它就會在拋出 MalformedURLException 異常時經過 getResourceByPath()
構造一個 ClassPathResource 類型的資源。而指定有協議前綴的資源路徑,則經過 URL 就能夠定義,因此返回的都是UrlResource類型。
從上面的示例咱們看到,其實 DefaultResourceLoader 對getResourceByPath(String)
方法處理其實不是很恰當,這個時候咱們可使用 FileSystemResourceLoader ,它繼承 DefaultResourceLoader 且覆寫了 getResourceByPath(String)
,使之從文件系統加載資源並以 FileSystemResource 類型返回,這樣咱們就能夠獲得想要的資源類型,以下:
@Override protected Resource getResourceByPath(String path) { if (path.startsWith("/")) { path = path.substring(1); } return new FileSystemContextResource(path); }
FileSystemContextResource 爲 FileSystemResourceLoader 的內部類,它繼承 FileSystemResource。
private static class FileSystemContextResource extends FileSystemResource implements ContextResource { public FileSystemContextResource(String path) { super(path); } @Override public String getPathWithinContext() { return getPath(); } }
在構造器中也是調用 FileSystemResource 的構造方法來構造 FileSystemResource 的。
若是將上面的示例將 DefaultResourceLoader 改成 FileSystemContextResource ,則 fileResource1 則爲 FileSystemResource。
ResourceLoader 的 Resource getResource(String location)
每次只能根據 location 返回一個 Resource,當須要加載多個資源時,咱們除了屢次調用 getResource()
外別無他法。ResourcePatternResolver 是 ResourceLoader 的擴展,它支持根據指定的資源路徑匹配模式每次返回多個 Resource 實例,其定義以下:
public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; Resource[] getResources(String locationPattern) throws IOException; }
ResourcePatternResolver 在 ResourceLoader 的基礎上增長了 getResources(String locationPattern)
,以支持根據路徑匹配模式返回多個 Resource 實例,同時也新增了一種新的協議前綴 classpath*:
,該協議前綴由其子類負責實現。
PathMatchingResourcePatternResolver 爲 ResourcePatternResolver 最經常使用的子類,它除了支持 ResourceLoader 和 ResourcePatternResolver 新增的 classpath*: 前綴外,還支持 Ant 風格的路徑匹配模式(相似於 **/*.xml
)。
PathMatchingResourcePatternResolver 提供了三個構造方法,以下:
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); }
PathMatchingResourcePatternResolver 在實例化的時候,能夠指定一個 ResourceLoader,若是不指定的話,它會在內部構造一個 DefaultResourceLoader。
Resource getResource(String location)
@Override public Resource getResource(String location) { return getResourceLoader().getResource(location); }
getResource()
方法直接委託給相應的 ResourceLoader 來實現,因此若是咱們在實例化的 PathMatchingResourcePatternResolver 的時候,若是不知道 ResourceLoader ,那麼在加載資源時,其實就是 DefaultResourceLoader 的過程。其實在下面介紹的 Resource[] getResources(String locationPattern)
也相同,只不過返回的資源時多個而已。
Resource[] getResources(String locationPattern)
public Resource[] getResources(String locationPattern) throws IOException { Assert.notNull(locationPattern, "Location pattern must not be null"); // 以 classpath*: 開頭 if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { // 路徑包含通配符 if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { return findPathMatchingResources(locationPattern); } else { // 路徑不包含通配符 return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } } else { int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(':') + 1); // 路徑包含通配符 if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { return findPathMatchingResources(locationPattern); } else { return new Resource[] {getResourceLoader().getResource(locationPattern)}; } } }
處理邏輯以下圖:
下面就 findAllClassPathResources()
、findAllClassPathResources()
作詳細分析。
findAllClassPathResources()
當 locationPattern 以 classpath*: 開頭可是不包含通配符,則調用findAllClassPathResources()
方法加載資源。該方法返回 classes 路徑下和全部 jar 包中的全部相匹配的資源。
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[0]); }
真正執行加載的是在 doFindAllClassPathResources()
方法,以下:
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)) { addAllClassLoaderJarRoots(cl, result); } return result; }
doFindAllClassPathResources()
根據 ClassLoader 加載路徑下的全部資源。在加載資源過程當中若是,在構造 PathMatchingResourcePatternResolver 實例的時候若是傳入了 ClassLoader,則調用其 getResources()
,不然調用ClassLoader.getSystemResources(path)
。 ClassLoader.getResources()
以下:
public Enumeration<URL> getResources(String name) throws IOException { @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); }
看到這裏是否是就已經一目瞭然了?若是當前父類加載器不爲 null,則經過父類向上迭代獲取資源,不然調用 getBootstrapResources()
。這裏是否是特別熟悉,(^▽^)。
若 path 爲 空(「」)時,則調用 addAllClassLoaderJarRoots()
方法。該方法主要是加載路徑下得全部 jar 包,方法較長也沒有什麼實際意義就不貼出來了。
經過上面的分析,咱們知道 findAllClassPathResources()
其實就是利用 ClassLoader 來加載指定路徑下的資源,不過它是在 class 路徑下仍是在 jar 包中。若是咱們傳入的路徑爲空或者 /
,則會調用 addAllClassLoaderJarRoots()
方法加載全部的 jar 包。
findAllClassPathResources()
當 locationPattern 以 classpath*: 開頭且當中包含了通配符,則調用該方法進行資源加載。以下:
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(); // bundle 資源類型 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.isDebugEnabled()) { logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result); } return result.toArray(new Resource[0]); }
方法有點兒長,可是思路仍是很清晰的,主要分兩步:
在這個方法裏面咱們要關注兩個方法,一個是 determineRootDir()
,一個是 doFindPathMatchingFileResources()
。
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); }
該方法必定要給出一個肯定的根目錄。該根目錄用於肯定文件的匹配的起始點,將根目錄位置的資源解析爲 java.io.File
並將其傳遞到 retrieveMatchingFiles()
,其他爲知用於模式匹配,找出咱們所須要的資源。
肯定根路徑以下:
原路徑 | 肯定根路徑 |
---|---|
classpath*:test/cc*/spring-*.xml |
classpath*:test/ |
classpath*:test/aa/spring-*.xml |
classpath*:test/aa/ |
肯定根路徑後,則調用 getResources()
方法獲取該路徑下得全部資源,而後迭代資源獲取符合條件的資源。
至此 Spring 整個資源記載過程已經分析完畢。下面簡要總結下:
Resource getResource(String location)
也實現了 Resource[] getResources(String locationPattern)
。