1. 程式人生 > >《Spring 5 官方文件》4. 資源

《Spring 5 官方文件》4. 資源

原文連結 譯者:XiaoLin

4.1 介紹

僅僅使用 JAVA 的 java.net.URL 和針對不同 URL 字首的標準處理器,並不能滿足我們對各種底層資源的訪問,比如:我們就不能通過 URL 的標準實現來訪問相對類路徑或者相對 ServletContext 的各種資源。雖然我們可以針對特定的 url 字首來註冊一個新的 URLStreamHandler(和現有的針對各種特定字首的處理器類似,比如 http:),然而這往往會是一件比較麻煩的事情(要求瞭解 url 的實現機制等),而且 url 介面也缺少了部分基本的方法,如檢查當前資源是否存在的方法。

相對標準 url 訪問機制,spring 的 Resource 介面對抽象底層資源的訪問提供了一套更好的機制。

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isOpen();

    URL getURL() throws IOException;

    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();

}
public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}

Resource 接口裡的最重要的幾個方法:

  • getInputStream(): 定位並且開啟當前資源,返回當前資源的 InputStream。預計每一次呼叫都會返回一個新的 – InputStream,因此關閉當前輸出流就成為了呼叫者的責任。
  • exists(): 返回一個 boolean,表示當前資源是否真的存在。
  • isOpen(): 返回一個 boolean,表示當前資源是否一個已開啟的輸入流。如果結果為 true,返回的 InputStream 不能多次讀取,只能是一次性讀取之後,就關閉 InputStream,以防止記憶體洩漏。除了 InputStreamResource,其他常用 Resource 實現都會返回 false。
  • getDescription(): 返回當前資源的描述,當處理資源出錯時,資源的描述會用於錯誤資訊的輸出。一般來說,資源的描述是一個完全限定的檔名稱,或者是當前資源的真實 url。

Resource 接口裡的其他方法可以讓你獲得代表當前資源的 URL 或 File 物件(前提是底層實現可相容的,也支援該功能)。

Resource抽象在Spring本身被廣泛使用,作為需要資源的許多方法簽名中的引數型別。 某些Spring API中的其他方法(例如各種ApplicationContext實現的建構函式)採用一個String,它以未安裝或簡單的形式用於建立適用於該上下文實現的資源,或者通過String路徑上的特殊字首,允許呼叫者 以指定必須建立和使用特定的資源實現。

Resource 介面(實現)不僅可以被 spring 大量的應用,其也非常適合作為你程式設計中訪問資源的輔助工具類。當你僅需要使用到 Resource 介面實現時,可以直接忽略 spring 的其餘部分。單獨使用 Rsourece 實現,會造成程式碼與 spring 的部分耦合,可也僅耦合了其中一小部分輔助類,而且你可以將 Reource 實現作為 URL 的一種訪問底層更為有效的替代,與你引入其他庫來達到這種目的是一樣的。

需要注意的是 Resource 實現並沒有去重新發明輪子,而是儘可能地採用封裝。舉個例子,UrlResource 裡就封裝了一個 URL 物件,在其內的邏輯就是通過封裝的 URL 物件來完成的。

spring 直接提供了多種開箱即用的 Resource 實現。

UrlResource 封裝了一個 java.net.URL 物件,用來訪問 URL 可以正常訪問的任意物件,比如檔案、an HTTP target, an FTP target, 等等。所有的 URL 都可以用一個標準化的字串來表示。如通過正確的標準化字首,可以用來表示當前 URL 的型別,當中就包括用於訪問檔案系統路徑的 file:,通過 http 協議訪問資源的 http:,通過 ftp 協議訪問資源的 ftp:,還有很多……

可以顯式化地使用 UrlResource 建構函式來建立一個 UrlResource,不過通常我們可以在呼叫一個 api 方法是,使用一個代表路徑的 String 引數來隱式建立一個 UrlResource。對於後一種情況,會由一個 javabean PropertyEditor 來決定建立哪一種 Resource。如果路徑裡包含某一個通用的字首(如 classpath:),PropertyEditor 會根據這個通用的字首來建立恰當的 Resource;反之,如果 PropertyEditor 無法識別這個字首,會把這個路徑作為一個標準的 URL 來建立一個 UrlResource。

ClassPathResource 可以從類路徑上載入資源,其可以使用執行緒上下文載入器、指定載入器或指定的 class 型別中的任意一個來載入資源。

當類路徑上資源存於檔案系統中,ClassPathResource 支援以 java.io.File 的形式訪問,可當類路徑上的資源存於尚未解壓(沒有 被Servlet 引擎或其他可解壓的環境解壓)的 jar 包中,ClassPathResource 就不再支援以 java.io.File 的形式訪問。鑑於上面所說這個問題,spring 中各式 Resource 實現都支援以 jave.net.URL 的形式訪問。

可以顯式使用 ClassPathResource 建構函式來建立一個 ClassPathResource ,不過通常我們可以在呼叫一個 api 方法時,使用一個代表路徑的 String 引數來隱式建立一個 ClassPathResource。對於後一種情況,會由一個 javabean PropertyEditor 來識別路徑中 classpath: 字首,從而建立一個 ClassPathResource。

這是針對 java.io.File 提供的 Resource 實現。顯然,我們可以使用 FileSystemResource 的 getFile() 函式獲取 File 物件,使用 getURL() 獲取 URL 物件。

這是為了獲取 web 根路徑的 ServletContext 資源而提供的 Resource 實現。

ServletContextResource 完全支援以流和 URL 的方式訪問,可只有當 web 專案是已解壓的(不是以 war 等壓縮包形式存在)且該 ServletContext 資源存於檔案系統裡,ServletContextResource 才支援以 java.io.File 的方式訪問。至於說到,我們的 web 專案是否已解壓和相關的 ServletContext 資源是否會存於檔案系統裡,這個取決於我們所使用的 Servlet 容器。若 Servlet 容器沒有解壓 web 專案,我們可以直接以 JAR 的形式的訪問,或者其他可以想到的方式(如訪問資料庫)等。

這是針對 InputStream 提供的 Resource 實現。建議,在確實沒有找到其他合適的 Resource 實現時,才使用 InputSteamResource。如果可以,儘量選擇 ByteArrayResource 或其他基於檔案的 Resource 實現來代替。

與其他 Resource 實現已比較,InputStreamRsource 倒像一個已開啟資源的描述符,因此,呼叫 isOpen() 方法會返回 true。除了在需要獲取資源的描述符或需要從輸入流多次讀取時,都不要使用 InputStreamResource 來讀取資源。

這是針對位元組陣列提供的 Resource 實現。可以通過一個位元組陣列來建立 ByteArrayResource。

當需要從位元組陣列載入內容時,ByteArrayResource 是一個不錯的選擇,使用 ByteArrayResource 可以不用求助於 InputStreamResource。

ResourceLoader 介面是用來載入 Resource 物件的,換句話說,就是當一個物件需要獲取 Resource 例項時,可以選擇實現 ResourceLoader 介面。

public interface ResourceLoader {

    Resource getResource(String location);

}

spring 裡所有的應用上下文都是實現了 ResourceLoader 介面,因此,所有應用上下文都可以通過 getResource() 方法獲取 Resource 例項。

當你在指定應用上下文呼叫 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 根據各種位置路徑載入資源的策略:

字首 例子 解釋
classpath: classpath:com/myapp/config.xml 從類路徑載入
(none) /data/config.xml 由底層的ApplicationContext實現決定

Table 4.1. Resource strings

ResourceLoaderAware 是一個特殊的標記介面,用來標記提供 ResourceLoader 引用的物件。

public interface ResourceLoaderAware {

    void setResourceLoader(ResourceLoader resourceLoader);
}

當將一個 ResourceLoaderAware 介面的實現類部署到應用上下文時(此類會作為一個 spring 管理的 bean), 應用上下文會識別出此為一個 ResourceLoaderAware 物件,並將自身作為一個引數來呼叫 setResourceLoader() 函式,如此,該實現類便可使用 ResourceLoader 獲取 Resource 例項來載入你所需要的資源。(附:為什麼能將應用上下文作為一個引數來呼叫 setResourceLoader() 函式呢?不要忘了,在前文有談過,spring 的所有上下文都實現了 ResourceLoader 介面)。

當然了,一個 bean 若想載入指定路徑下的資源,除了剛才提到的實現 ResourcesLoaderAware 介面之外(將 ApplicationContext 作為一個 ResourceLoader 物件注入),bean 也可以實現 ApplicationContextAware 介面,這樣可以直接使用應用上下文來載入資源。但總的來說,在需求滿足都滿足的情況下,最好是使用的專用 ResourceLoader 介面,因為這樣程式碼只會與介面耦合,而不會與整個 spring ApplicationContext 耦合。與 ResourceLoader 介面耦合,拋開 spring 來看,就是提供了一個載入資源的工具類介面。

從 spring 2.5 開始,除了實現 ResourceLoaderAware 介面,也可採取另外一種替代方案——依賴於 ResourceLoader 的自動裝配。”傳統”的 constructor 和 bytype 自動裝配模式都支援 ResourceLoader 的裝配(可參閱 Section 5.4.5, “自動裝配協作者” )——前者以構造引數的形式裝配,後者以 setter 方法中引數裝配。若為了獲得更大的靈活性(包括屬性注入的能力和多參方法),可以考慮使用基於註解的新注入方式。使用註解 @Autowiring 標記 ResourceLoader 變數,便可將其注入到成員屬性、構造引數或方法引數中( @autowiring 詳細的使用方法可參考Section 3.9.2, “@Autowired”.)。

如果bean本身將通過某種動態過程來確定和提供資源路徑,那麼bean可以使用ResourceLoader介面來載入資源。 j假設以某種方式載入一個模板,其中需要的特定資源取決於使用者的角色。 如果資源是靜態的,那麼完全消除ResourceLoader介面的使用是有意義的,只需讓bean公開它需要的Resource屬性,那麼它們就會以你所期望的方式被注入。

什麼使得它們輕鬆注入這些屬性,是所有應用程式上下文註冊和使用一個特殊的JavaBeans PropertyEditor,它可以將String路徑轉換為Resource物件。 因此,如果myBean具有“資源”型別的模板屬性,則可以使用該資源的簡單字串進行配置,如下所示:

<bean id="myBean" class="...">
<property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

請注意,資源路徑沒有字首,因為應用程式上下文字身將用作ResourceLoader,資源本身將通過ClassPathResource,FileSystemResource或ServletContextResource(根據需要)載入,具體取決於上下文的確切型別。

如果需要強制使用特定的資源型別,則可以使用字首。 以下兩個示例顯示如何強制使用ClassPathResource和UrlResource(後者用於訪問檔案系統檔案)。

<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>

(某一特定)應用上下文的構造器通常可以使用字串或字串陣列所指代的(多個)資源(如 xml 檔案)來構造當前上下文。

當指定的位置路徑沒有帶字首時,那從指定位置路徑建立的 Resource 型別(用於後續載入 bean 定義),取決於所使用應用上下文。舉個列子,如下所建立的 ClassPathXmlApplicationContext :

ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

會從類路徑載入 bean 的定義,因為所建立的 Resource 例項是 ClassPathResource.但所建立的是 FileSystemXmlApplicationContext 時,

ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");

則會從檔案系統載入 bean 的定義,這種情況下,資源路徑是相對工作目錄而言的。

注意:若位置路徑帶有 classpath 字首或 URL 字首,會覆蓋預設建立的用於載入 bean 定義的 Resource 型別,比如這種情況下的 FileSystemXmlApplicationContext

ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");

,實際是從類路徑下載入了 bean 的定義。可是,這個上下文仍然是 FileSystemXmlApplicationContext,而不是 ClassPathXmlApplicationContext,在後續作為 ResourceLoader 來使用時,不帶字首的路徑仍然會從檔案系統中載入。

構造 ClassPathXmlApplicationContext 例項 – 快捷方式

ClassPathXmlApplicationContext 提供了多個建構函式,以利於快捷建立 ClassPathXmlApplicationContext 的例項。最好莫不過使用只包含多個 xml 檔名(不帶路徑資訊)的字串陣列和一個 Class 引數的構造器,所省略路徑資訊 ClassPathXmlApplicationContext 會從 Class 引數 獲取:

下面的這個例子,可以讓你對個構造器有比較清晰的認識。試想一個如下類似的目錄結構:

com/
  foo/
	services.xml
	daos.xml
    MessengerService.class

由 services.xml 和 daos.xml 中 bean 所組成的 ClassPathXmlApplicationContext,可以這樣來初始化:

ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}, MessengerService.class);

欲要知道 ClassPathXmlApplicationContext 更多不同型別的構造器,請查閱 Javadocs 文件。

從前文可知,應用上下文構造器的中的資源路徑可以是單一的路徑(即一對一地對映到目標資源);另外資源路徑也可以使用高效的萬用字元——可包含 classpath*:字首 或 ant 風格的正則表示式(使用 spring 的 PathMatcher 來匹配)。

萬用字元機制的其中一種應用可以用來組裝元件式的應用程式。應用程式裡所有元件都可以在一個共知的位置路徑釋出自定義的上下文片段,則最終應用上下文可使用 classpath*: 在同一路徑字首(前面的共知路徑)下建立,這時所有元件上下文的片段都會被自動組裝。

謹記,路徑中的萬用字元特定用於應用上下文的構造器,只會在應用構造時有效,與其 Resource 自身型別沒有任何關係。不可以使用 classpth*:來構造任一真實的 Resource,因為一個資源點一次只可以指向一個資源。(如果直接使用 PathMatcher 的工具類,也可以在路徑中使用萬用字元)

以下是一些使用了 Ant 風格的位置路徑:

/WEB-INF/*-context.xml
  com/mycompany/**/applicationContext.xml
  file:C:/some/path/*-context.xml
  classpath:com/mycompany/**/applicationContext.xml

當位置路徑使用了 ant 風格,直譯器會遵循一套複雜且預定義的邏輯來解釋這些位置路徑。直譯器會先從位置路徑裡獲取最靠前的不帶萬用字元的路徑片段,使用這個路徑片段來建立一個 Resource ,並從 Resource 裡獲取其 URL,若所獲取到 URL 字首並不是 “jar:”,或其他特殊容器產生的特殊字首(如 WebLogic 的 zip:,WebSphere wsjar),則從 Resource 裡獲取 java.io.File 物件,並通過其遍歷檔案系統。進而解決位置路徑裡萬用字元;若獲取的是 “jar:”的 URL ,解析器會從其獲取一個 java.net.JarURLConnection 或手動解析此 URL,並遍歷 jar 檔案的內容進而解決位置路徑的萬用字元。

如果指定的路徑已經是檔案URL(顯式地或隱含地,因為基本的ResourceLoader是一個檔案系統的,那麼萬用字元將保證以完全可移植的方式工作。

如果指定的路徑是類路徑位置,則解析器必須通過Classloader.getResource()呼叫獲取最後一個非萬用字元路徑段URL。 由於這只是路徑的一個節點(而不是最後的檔案),在這種情況下,它實際上是未定義的(在ClassLoader javadocs中)返回的是什麼樣的URL。 實際上,它始終是一個java.io.File,它表示類路徑資源解析為檔案系統位置的目錄或某種型別的jar URL,其中類路徑資源解析為一個jar位置。 儘管如此,這種操作仍然存在可移植性問題。

如果為最後一個非萬用字元段獲取了一個jar URL,解析器必須能夠從中獲取java.net.JarURLConnection,或者手動解析jar URL,以便能夠遍歷該jar的內容,然後解析 萬用字元。 這將在大多數環境中正常工作,但在其他環境中將會失敗,並且強烈建議您在依賴它之前,徹底地在您的特定環境中徹底測試來自jar的資源的萬用字元解析。

當構造基於 xml 檔案的應用上下文時,位置路徑可以使用 classpath*:字首:

ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

classpath*:的使用表示類路徑下所有匹配檔名稱的資源都會被獲取(本質上就是呼叫了 ClassLoader.getResources(…) 方法),接著將獲取到的資源組裝成最終的應用上下文。

萬用字元路徑依賴了底層 classloader 的 getResource 方法。可是現在大多數應用伺服器提供了自身的 classloader 實現,其處理 jar 檔案的形式可能各有不同。要在指定伺服器測試 classpath*: 是否有效,簡單點可以使用 getClass().getClassLoader().getResources(“”) 去載入類路徑 jar包裡的一個檔案。嘗試在兩個不同的路徑載入名稱相同的檔案,如果返回的結果不一致,就需要檢視一下此伺服器中與 classloader 行為設定相關的文件。

在位置路徑的其餘部分,classpath*: 字首可以與 PathMatcher 結合使用,如:” classpath*:META-INF/*-beans.xml”。這種情況的解析策略非常簡單:取位置路徑最靠前的無萬用字元片段,呼叫 ClassLoader.getResources() 獲取所有匹配的類層次載入器可載入的的資源,隨後將 PathMacher 的策略應用於每一個獲得的資源(起過濾作用)。

除非所有目標資源都存於檔案系統,否則classpath*:和 ant 風格模式的結合使用,都只能在至少有一個確定根包路徑的情況下,才能達到預期的效果。換句話說,就是像 classpath*:*.xml 這樣的 pattern 不能從根目錄的 jar 檔案中獲取資源,只能從根目錄的擴充套件目錄獲取資源。此問題的造成源於 jdk ClassLoader.getResources() 方法的侷限性——當向 ClassLoader.getResources() 傳入空串時(表示搜尋潛在的根目錄),只能獲取的檔案系統的檔案位置路徑,即獲取不了 jar 中檔案的位置路徑。

如果在多個類路徑上存在所搜尋的根包,那使用 classpath: 和 ant 風格模式一起指定的資源不保證找到匹配的資源。因為使用如下的 pattern classpath:com/mycompany/**/service-context.xml
去搜索只在某一個路徑存在的指定資源com/mycompany/package1/service-context.xml
時,解析器只會對 getResource(“com/mycompany”) 返回的(第一個) URL 進行遍歷和解釋,則當在多個類路徑存在基礎包節點 “com/mycompany” 時(如在多個 jar 存在這個基礎節點),解析器就不一定會找到指定資源。因此,這種情況下建議結合使用 classpath*: 和 ant 風格模式,classpath*:會讓解析器去搜索所有包含基礎包節點的類路徑。

FileSystemResource 沒有依附 FileSystemApplicationContext,因為 FileSystemApplicationContext 並不是一個真正的 `ResourceLoader。FileSystemResource 並沒有按約定規則來處理絕對和相對路徑。相對路徑是相對與當前工作而言,而絕對路徑則是相對檔案系統的根目錄而言。

然而為了向後相容,當 FileSystemApplicationContext 是一個 ResourceLoader 例項時,我們做了一些改變 —— 不管 FileSystemResource` 例項的位置路徑是否以 / 開頭, FileSystemApplicationContext 都強制將其作為相對路徑來處理。事實上,這意味著以下例子等效:

ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/context.xml");
ApplicationContext ctx = new FileSystemXmlApplicationContext("/conf/context.xml");

還有:(即使它們的意義不一樣 —— 一個是相對路徑,另一個是絕對路徑。)

FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");

實踐中,如果確實需要使用絕對路徑,建議放棄 FileSystemResource / FileSystemXmlApplicationContext 在絕對路勁的使用,而強制使用 file: 的 UrlResource。

// Resource 只會是 UrlResource,與上下文的真實型別無關
ctx.getResource("file:///some/resource/path/myTemplate.txt");
// 強制 FileSystemXmlApplicationContext 通過 UrlResource 載入資源
ApplicationContext ctx = new FileSystemXmlApplicationContext("file:///conf/context.xml");