1. 程式人生 > >Spring中資源的載入原來是這麼一回事啊!

Spring中資源的載入原來是這麼一回事啊!

## 1. 簡介 在JDK中 `java.net.URL` 適用於載入資源的類,但是 `URL` 的實現類都是訪問網路資源的,並沒有可以從類路徑或者相對路徑獲取檔案及 `ServletContext` , 雖然可以通過自定義擴充套件URL介面來實現新的處理程式,但是這是非常複雜的,同時 `URL` 介面中定義的行為也並不是很理想的 ,如檢測資源的存在等的行為,這也是 `spring` 為什麼自己全新的開發一套自己的資源載入策略, 同時它也滿足下面的特點: * 單一職責原則,將資源的定義和資源的載入的**模型界限**劃的非常清晰 * 採用高度抽象,統一的資源定義和資源載入的策略和行為,資源載入返回給客戶端的是抽象的資源,客戶端根據資源的行為定義對其進行具體化的處理 ## 2. Resource 介面 `spring` 中的 `Resource` 介面目的在於成為一種功能更加強大的介面,用於抽象化對具體資源的訪問,它繼承了 `org.springframework.core.io.InputStreamSource` 介面,作為資源定義的頂級介面, `Resource` 內部定義了通用的方法,並且有它的子類 `AbstractResource` 來提供統一的預設實現, `Resouerce` 介面定義: ``` JAVA //資源定義介面 public interface Resource extends InputStreamSource { /** * 檢驗資源是否是物理存在 */ boolean exists(); /** * 判斷資源是否是可讀的 */ default boolean isReadable() { return exists(); } /** * 判斷資源是否是開啟的,true為開啟 */ default boolean isOpen() { return false; } /** * 判斷該資源是否是檔案 true為是 */ default boolean isFile() { return false; } /** * 返回該資源的URL控制代碼 */ URL getURL() throws IOException; /** * 返回該資源的URI控制代碼 */ URI getURI() throws IOException; /** * 獲取該資源的File控制代碼 */ File getFile() throws IOException; /** * 返回一個ReadableByteChannel 作為NIO中的可讀通道 */ 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(); } ``` `InputStreamSource ` 介面定義: ``` JAVA public interface InputStreamSource { /** * Return an {@link InputStream} for the content of an underlying resource. *

It is expected that each call creates a fresh stream. *

This requirement is particularly important when you consider an API such * as JavaMail, which needs to be able to read the stream multiple times when * creating mail attachments. For such a use case, it is required * that each {@code getInputStream()} call returns a fresh stream. * @return the input stream for the underlying resource (must not be {@code null}) * @throws java.io.FileNotFoundException if the underlying resource doesn't exist * @throws IOException if the content stream could not be opened */ InputStream getInputStream() throws IOException; } ``` 該 `Resource` 中一些最重要的方法: * `getInputStream()` :找到並開啟資源,並返回一個資源以 `InputStream` 供讀取,每次呼叫都會返回一個新的 `InputStream` ,呼叫者有責任關閉流 * `exists()` :返回 `boolean` 指示此資源是否實際以物理形式存在。 * `isOpen()` :返回, `boolean` 指示此資源是否表示具有開啟流的控制代碼, 如果為 `true` , `InputStream` 則不能多次讀取,必須只讀取一次,然後將其關閉以**避免資源洩漏**。返回 `false` 所有常用資源實現(除外) `InputStreamResource` 可讀 * `getDescription()` :返回對此資源的描述,以便在使用該資源時用於錯誤輸出。這通常是標準檔名或資源的實際 `URL` ** `Resource` 實現** ![Resource類圖](https://img2020.cnblogs.com/other/2024393/202005/2024393-20200507200830052-2128933228.png) - `UrlResource` : 包裝一個 `java.net.URL` ,可用於訪問通常可以通過 `URL` 訪問的任何物件,例如檔案, `HTTP` 目標, `FTP` 目標等。所有 `URL` 都有一個標準化的 `String` 表示形式,因此適當的標準化字首可用於指示另一種 `URL` 型別。如: `file` : 訪問檔案系統路徑, `http` : 通過 `HTTP` 協議 `ftp` : 訪問資源,通過 `FTP` 訪問資源等 - `ClassPathResource` : 此類表示應從類路徑獲取的資源。它使用執行緒上下文類載入器( `ClassLoader` ),給定的類載入器或給定的類來載入資源 - `FileSystemResource` : 是一個 `Resource` 執行 `java.io.File` 和 `java.nio.file.Path` 型別資源的封裝,它支援 `File` 和 `URL` , 實現 `WritableResource` 介面,且從 `Spring Framework 5.0` 開始, `FileSystemResource` 使用 `NIO2 API` 進行讀/寫互動 - `ServletContextResource` : 該 `ServletContex` t資源解釋相關 `Web` 應用程式的根目錄內的相對路徑。 - `InputStreamResource` : 將給定的 InputStream 作為一種資源的 Resource 的實現類 - `ByteArrayResource` : 這是Resource給定位元組陣列的實現。它為給定的位元組陣列建立一個 `ByteArrayInputStream` ## 3. ResourceLoader 介面 `ResourceLoader` 主要是用於返回(即載入) `Resource` 物件,主要定義: ``` JAVA public interface ResourceLoader { /** Pseudo URL prefix for loading from the class path: "classpath:". */ String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; /** * 返回指定路徑的資源處理器 * 必須支援完全限定的網址: "file:C:/test.dat" * 必須支援ClassPath 的 URL :"classpath:test.dat" * 必須支援相對路徑 : "WEB-INF/test.dat" * 並不能保證資源是否物理存在,需要自己去檢測通過existence * 再spring中所有的應用上下文都去實現這個介面,可以進行資源的載入 */ Resource getResource(String location); /** * 返回當前類的 ClassLoader 物件 */ @Nullable ClassLoader getClassLoader(); } ``` - 應用上下文即容器都有實現 `ResourceLoader` 這個介面,所有的上下文都可以用於獲取 `Resource` 例項物件 - 我們可以在特定的應用上下文中通過 `getResource()` 來獲取特定型別的 `Resource` 例項,但是的保證 `location` 路徑沒有特殊的字首,如 `classpatch:` 等,如果有特定字首慢麼會強制使用相應的資源型別,與上下文無關。 | Prefix | Example | Explanation | |------------|--------------------------------|---------------------| | classpath: | classpath:com/myapp/config.xml | 從類路徑載入 | | file: | file:///data/config.xml | 從檔案系統作為 `URL` 載入 | | http: | https://myserver/logo.png | 按照URL形式載入 | | (none) | /data/config.xml | 取決於應用上下文 | `ResourceLoader` 的子類結構: ![](https://img2020.cnblogs.com/other/2024393/202005/2024393-20200507200831108-874196323.png) ### 3.1 DefaultResourceLoader 這個類是 `ResourceLoader` 的預設實現類,與 `Resource` 介面的 `AbstractResource` 一樣, #### 3.1.1. 建構函式 - 提供有參和無參的建構函式,有參建構函式接受 `ClassLoader` 型別,如不帶引數則使用預設的 `ClassLoader` , `Thread.currentThread()#getContextClassLoader()` 核心程式碼程式碼,部分省去: ``` JAVA public class DefaultResourceLoader implements ResourceLoader { @Nullable private ClassLoader classLoader; private final Set

Any such resolver will be invoked ahead of this loader's standard * resolution rules. It may therefore also override any default rules. * @since 4.3 * @see #getProtocolResolvers() */ public void addProtocolResolver(ProtocolResolver resolver) { Assert.notNull(resolver, "ProtocolResolver must not be null"); this.protocolResolvers.add(resolver); } ``` ### 3.2 FileSystemResourceLoader 在 `DefaultResourceLoader` 中 `getResourceByPath()` 方法的處理是直接返回了一個 `ClassPathContextResource` 型別的資源,這其實是不完善的,在spring中 `FileSystemResourceLoader` 類繼承了 `DefaultResourceLoader` ,同時重寫了 `getResourceByPath()` 方法,使用標準的檔案系統讀入,並且返回 `FileSystemContextResource` 型別 ``` JAVA public class FileSystemResourceLoader extends DefaultResourceLoader { /** * Resolve resource paths as file system paths. *

Note: Even if a given path starts with a slash, it will get * interpreted as relative to the current VM working directory. * @param path the path to the resource * @return the corresponding Resource handle * @see FileSystemResource * @see org.springframework.web.context.support.ServletContextResourceLoader#getResourceByPath */ @Override protected Resource getResourceByPath(String path) { if (path.startsWith("/")) { path = path.substring(1); } return new FileSystemContextResource(path); } /** * FileSystemResource that explicitly expresses a context-relative path * through implementing the ContextResource interface. */ private static class FileSystemContextResource extends FileSystemResource implements ContextResource { public FileSystemContextResource(String path) { super(path); } @Override public String getPathWithinContext() { return getPath(); } } } ``` * 我們可以從 上面的程式碼中看到 在 `FileSystemResourceLoader` 中有一個私有的內部類 `FileSystemContextResource` , 這個類繼承了 `FileSystemResource` ,同時實現了 `ContextResource` 介面 * `FileSystemContextResource` 通過建構函式呼叫 `FileSystemResource` 的建構函式,建立 `FileSystemResource` 型別資源定義,同時實現 `ContextResource` 是為了實現其中的 `getPathWithinContext()` 方法,這個方法是用來獲取上下文根路徑的, 原始碼中這樣寫的 : > /** * Return the path within the enclosing 'context'. * This is typically path relative to a context-specific root directory, * e.g. a ServletContext root or a PortletContext root. */ ### 3.3 ClassRelativeResourceLoader `org.springframework.core.io.ClassRelativeResourceLoader` 類也是 `DefaultResourceLoader` 的另一個實現子類,與 `FileSystemResourceLoader` 類似,也同樣重寫了 `getResourceByPath()` 方法,也內部維護了一個私有的內部類 `ClassRelativeContextResource` , 具體程式碼如下: ``` JAVA /** * 從給定的 class 下載入資源 * {@link ResourceLoader} implementation that interprets plain resource paths * as relative to a given {@code java.lang.Class}. * * @author Juergen Hoeller * @since 3.0 * @see Class#getResource(String) * @see ClassPathResource#ClassPathResource(String, Class) */ public class ClassRelativeResourceLoader extends DefaultResourceLoader { private final Class clazz; /** * Create a new ClassRelativeResourceLoader for the given class. * @param clazz the class to load resources through */ public ClassRelativeResourceLoader(Class clazz) { Assert.notNull(clazz, "Class must not be null"); this.clazz = clazz; setClassLoader(clazz.getClassLoader()); } /** * 重寫getResourceByPath 方法 , 返回一個ClassRelativeContextResource 資源型別 * @param path the path to the resource * @return */ @Override protected Resource getResourceByPath(String path) { return new ClassRelativeContextResource(path, this.clazz); } /** * 繼承 ClassPathResource 定義資源型別,實現ContextResource 中的 getPathWithinContext 方法, * * ClassPathResource that explicitly expresses a context-relative path * through implementing the ContextResource interface. */ private static class ClassRelativeContextResource extends ClassPathResource implements ContextResource { private final Class clazz; /** * 呼叫父類 ClassPathResource 對資源進行初始化 * @param path * @param clazz */ public ClassRelativeContextResource(String path, Class clazz) { super(path, clazz); this.clazz = clazz; } @Override public String getPathWithinContext() { return getPath(); } /** * 重寫 ClassPathContext 中方法, 通過給定的路徑返回一個ClassRelativeContextResource資源 * @param relativePath the relative path (relative to this resource) * @return */ @Override public Resource createRelative(String relativePath) { String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath); return new ClassRelativeContextResource(pathToUse, this.clazz); } } } ``` ### 3.4 ResourcePatternResolver `org.springframework.core.io.support.ResourcePatternResolver` 是對 `ResourceLoader` 的一個擴充套件,我們在 `ResourceLoader` 中通過 `getResource` 方法獲取 `Resource` 例項時,只能通過一個 `location` 來獲取一個 `Resource` , 而不能獲取到多個 `Resource` , 當我們需要載入多個資源時,只能通過呼叫多次的該方法來實現,所以spring 提供了 `ResourcePatternResolver` 對其進行了擴充套件,實現了通過 `location` 來載入多個資源,類的定義如下: ``` JAVA public interface ResourcePatternResolver extends ResourceLoader { /** * Pseudo URL prefix for all matching resources from the class path: "classpath*:" * This differs from ResourceLoader's classpath URL prefix in that it * retrieves all matching resources for a given name (e.g. "/beans.xml"), * for example in the root of all deployed JAR files. * @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX */ String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; /** * Resolve the given location pattern into Resource objects. *

Overlapping resource entries that point to the same physical * resource should be avoided, as far as possible. The result should * have set semantics. * @param locationPattern the location pattern to resolve * @return the corresponding Resource objects * @throws IOException in case of I/O errors */ Resource[] getResources(String locationPattern) throws IOException; } ``` * 可以看到 `ResourcePatternResolver` 新增加了一個方法 `getResources` ,返回一個 `Resource` 陣列 * 這裡我們要注意, `ResourcePatternResolver` 增加了一個新的協議字首 `classpath*:` , 看到這裡是不是大家可以很熟悉的想起我們在平時配置路徑時經常會寫 `classpath:` 和 `classpath*:` ,那麼他們的區別就在這裡,他們的資源載入方式時不一樣的 ### 3.5 PathMatchingResourcePatternResolver `org.springframework.core.io.support.PathMatchingResourcePatternResolver` 是 `ResourcePatternResolver` 的一個主要實現類,也是使用較多的一個實現類,我們可以來看一下,它主要實現了 新增字首的解析,同時還支援 `Ant` 風格的路徑匹配模式(如 : `"**/*.xml"` ) #### 3.5.1 建構函式 `PathMatchingResourcePatternResolver` 提供了三個建構函式: ``` JAVA /** * 內建 資源定位載入器 */ private final ResourceLoader resourceLoader; /** * Ant路徑匹配器 */ private PathMatcher pathMatcher = new AntPathMatcher(); /** * 無參建構函式,當不指定內部載入器型別時,預設是 DefaultResourceLoader * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. *

ClassLoader access will happen via the thread context class loader. * @see org.springframework.core.io.DefaultResourceLoader */ public PathMatchingResourcePatternResolver() { this.resourceLoader = new DefaultResourceLoader(); } /** * 指定特定的資源定位載入器 * Create a new PathMatchingResourcePatternResolver. *

ClassLoader access will happen via the thread context class loader. * @param resourceLoader the ResourceLoader to load root directories and * actual resources with */ public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { Assert.notNull(resourceLoader, "ResourceLoader must not be null"); this.resourceLoader = resourceLoader; } /** * 使用預設的資源載入器,但是傳入 classLoader ,使用特定的類載入 * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. * @param classLoader the ClassLoader to load classpath resources with, * or {@code null} for using the thread context class loader * at the time of actual resource access * @see org.springframework.core.io.DefaultResourceLoader */ public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) { this.resourceLoader = new DefaultResourceLoader(classLoader); } ``` * 我們可以看到,當建構函式不提供 `ResourceLoader` 時,預設是 `DefaultResourceLoader` #### 3.5.2 getResource `PathMatchingResourcePatternResolver` 中的 `getResource` 方法的實現是呼叫了 傳入的 `ResourceLoader` 或者預設的 `DefaultResourceLoader` , 具體的程式碼實現如下: ``` JAVA /** * 呼叫getResourceLoader 獲取當前的 ResourceLoader * @param location the resource location * @return */ @Override public Resource getResource(String location) { return getResourceLoader().getResource(location); } /** * Return the ResourceLoader that this pattern resolver works with. */ public ResourceLoader getResourceLoader() { return this.resourceLoader; } ``` #### 3.5.3 getResources 實現了 `ResourcePatternResolver` 的 `getResources` 方法,可以通過 `location` 載入多個資源,進行分類處理,如果是沒有 `classpath*:` 字首以及不包含萬用字元的情況下直接呼叫當前類的 `ResourceLoader` 來進行處理,其他按具體來處理,主要涉及兩個方法 `#findPathMatchingResources(...)` 與 `#findAllClassPathResources(...)` ``` JAVA @Override public Resource[] getResources(String locationPattern) throws IOException { Assert.notNull(locationPattern, "Location pattern must not be null"); //1. 判斷 是不是classpath* 開頭的 if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { //1.1.進行路徑匹配校驗 是否包含萬用字元 // 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 { //1.2 不包含萬用字元 // all class path resources with the given name return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } } else { // 2. 不是classpath字首開頭 // 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); //2.1 校驗是否包含萬用字元 if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { // a file pattern return findPathMatchingResources(locationPattern); } else { //2.2 不包含萬用字元 使用內部 ResourceLoader 進行資源載入 預設是 DefaultReourceLoader // a single resource with the given name return new Resource[] {getResourceLoader().getResource(locationPattern)}; } } } ``` #### 3.5.4 findPathMatchingResources 上面程式碼中我們可以看到,當存在萬用字元時都會執行 `#findPathMatchingResources(...)` 方法,我們來看一下方法的定義: ``` JAVA /** * 通過ant解析器來對給定的路徑下的所有模糊資源進行解析和匹配 * 支援jar和zip以及系統中的檔案資源 * Find all resources that match the given location pattern via the * Ant-style PathMatcher. Supports resources in jar files and zip files * and in the file system. * @param locationPattern the location pattern to match * @return the result as Resource array * @throws IOException in case of I/O errors * @see #doFindPathMatchingJarResources * @see #doFindPathMatchingFileResources * @see org.springframework.util.PathMatcher */ protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { //解析根路徑 String rootDirPath = determineRootDir(locationPattern); //解析到子路徑 String subPattern = locationPattern.substring(rootDirPath.length()); //獲取根路徑的資源 Resource[] rootDirResources = getResources(rootDirPath); Set

Used for determining the starting point for file matching, * resolving the root directory location to a {@code java.io.File} * and passing it into {@code retrieveMatchingFiles}, with the * remainder of the location as pattern. *

Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml", * for example. * @param location the location to check * @return the part of the location that denotes the root directory * @see #retrieveMatchingFiles */ protected String determineRootDir(String location) { //1. 找到最後 路徑中出現的 : 的索引 +1 ,這裡注意我們的路徑時 類似 : classpath*: /web-inf/*.xml int prefixEnd = location.indexOf(':') + 1; //2. 獲取跟路徑長度 int rootDirEnd = location.length(); //3.判斷冒號後面的路徑是否包含萬用字元 如果包含,則截斷最後一個由”/”分割的部分。 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*:/test/aa*/app-*.xml` | `classpath*:/test/` | | `classpath*:/test/aa/app-*.xml` | `classpath*:/test/aa` | ##### 3.5.4.2 doFindPathMatchingFileResources(... ) `#doFindPathMatchingFileResources(...)` 、 `#doFindPathMatchingJarResources(...)` 方法的的內部基本一致,只是解析不同的型別檔案,我們這裡只看其中一個則可,大家可以自行比對兩者的區別。 - 我們跟一下 `#doFindPathMatchingFileResources(...)` 方法,方法內部呼叫較深,所以下面我主要把程式碼貼出來,註釋已有,相信可以看的懂 - `#doFindPathMatchingFileResources(...)` 程式碼: ``` JAVA /** * 查詢檔案系統符合給定的location的資源, 路徑符合 ant 樣式的萬用字元 * Find all resources in the file system that match the given location pattern * via the Ant-style PathMatcher. * @param rootDirResource the root directory as Resource * @param subPattern the sub pattern to match (below the root directory) * @return a mutable Set of matching Resource instances * @throws IOException in case of I/O errors * @see #retrieveMatchingFiles * @see org.springframework.util.PathMatcher */ protected