1. 程式人生 > >Spring Resource框架體系介紹

Spring Resource框架體系介紹

Resource介紹

在使用spring作為容器進行專案開發中會有很多的配置檔案,這些配置檔案都是通過Spring的Resource介面來實現載入,但是,Resource對於所有低階資源的訪問都不夠充分。例如,沒有標準化的URL實現可用於訪問需要從類路徑或相對於ServletContext獲取的資源。(更多關於ServletContext的理解,請訪問https://www.cnblogs.com/cxuanBlog/p/10927813.html)雖然可以為專用的URL字首註冊新的處理程式(類似於http :)這樣的字首的現有處理程式,但這通常非常複雜,並且URL介面仍然缺少一些理想的功能,例如檢查存在的方法被指向的資源。

JavaDoc解釋

從實際型別的底層資源(例如檔案或類路徑資源)中抽象出來的資源描述符的介面。

Resource介面方法

Spring的Resource介面旨在成為一個更有能力的介面,用於抽象對低階資源的訪問。以下清單顯示了Resource介面定義

public interface Resource extends InputStreamSource {
 
  boolean exists();
  
  default boolean isReadable() {
        return true;
    }
  
  default boolean isOpen() {
        return false;
    }
  
  default boolean isFile() {
        return false;
    }
  
  URL getURL() throws IOException;
  
  URI getURI() throws IOException;
  
  File getFile() throws IOException;
  
  default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(getInputStream());
    }
  
  long contentLength() throws IOException;
  
  long lastModified() throws IOException;
  
  Resource createRelative(String relativePath) throws IOException;
  
    String getFilename();
  
  String getDescription();
}

Resource介面繼承了InputStreamSource介面,提供了很多InputStreamSource所沒有的方法

下面來看一下InputStreamSource介面,只有一個方法

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}

其中一些大部分重要的介面是:

  • getInputStream(): 找到並開啟資源,返回一個InputStream以從資源中讀取。預計每次呼叫都會返回一個新的InputStream(),呼叫者有責任關閉每個流
  • exists()
    : 返回一個布林值,表明某個資源是否以物理形式存在
  • isOpen: 返回一個布林值,指示此資源是否具有開放流的控制代碼。如果為true,InputStream就不能夠多次讀取,只能夠讀取一次並且及時關閉以避免記憶體洩漏。對於所有常規資源實現,返回false,但是InputStreamResource除外。
  • getDescription(): 返回資源的描述,用來輸出錯誤的日誌。這通常是完全限定的檔名或資源的實際URL。

其他方法:

  • isReadable(): 表明資源的目錄讀取是否通過getInputStream()進行讀取。
  • isFile(): 表明這個資源是否代表了一個檔案系統的檔案。
  • getURL(): 返回一個URL控制代碼,如果資源不能夠被解析為URL,將丟擲IOException
  • getURI(): 返回一個資源的URI控制代碼
  • getFile(): 返回某個檔案,如果資源不能夠被解析稱為絕對路徑,將會丟擲FileNotFoundException
  • lastModified(): 資源最後一次修改的時間戳
  • createRelative(): 建立此資源的相關資源
  • getFilename(): 資源的檔名是什麼 例如:最後一部分的檔名 myfile.txt

Resource的實現類

Resource 介面是 Spring 資源訪問策略的抽象,它本身並不提供任何資源訪問實現,具體的資源訪問由該介面的實現類完成——每個實現類代表一種資源訪問策略。

基礎類介紹

Resource一般包括這些實現類:UrlResource、ClassPathResource、FileSystemResource、ServletContextResource、InputStreamResource、ByteArrayResource

使用UrlResource訪問網路資源

訪問網路資源的實現類。Resource的一個實現類用來定位URL中的資源。它支援URL的絕對路徑,用來作為file: 埠的一個資源,建立一個maven專案,配置Spring依賴(不再贅述)和dom4j 的依賴,並在根目錄下建立一個books.xml。

程式碼表示:

public class UrlResourceTest {

    public static void loadAndReadUrlResource(String path) throws Exception{
        // 建立一個 Resource 物件,指定從檔案系統裡讀取資源,相對路徑
        UrlResource resource = new UrlResource(path);
        // 絕對路徑
//        UrlResource resource = new UrlResource("file:///Users/mr.l/test/CXuan-Spring/CXuan-Spring-Resource/books.xml");
        // 獲取檔名
        System.out.println("resource.getFileName = " + resource.getFilename());
        // 獲取檔案描述
        System.out.println("resource.getDescription = "+ resource.getDescription());
        SAXReader reader = new SAXReader();
        System.out.println(resource.getFile());
        Document document = reader.read("file:///Users/mr.l/test/CXuan-Spring/CXuan-Spring-Resource/books.xml");
        Element parent = document.getRootElement();
        List<Element> elements = parent.elements();
        for(Element element : elements){
            // 獲取name,description,price
            System.out.println(element.getName() + " = " +element.getText());
        }
    }


    public static void main(String[] args) throws Exception {
        loadAndReadUrlResource("file:books.xml");
    }

}

上面程式使用UrlResource來訪問網路資源,也可以通過file 字首訪問本地資源,上述程式碼就是這樣做的。如果要訪問網路資源,可以有兩種形式

  • http:-該字首用於訪問基於 HTTP 協議的網路資源。
  • ftp:-該字首用於訪問基於 FTP 協議的網路資源。

使用ClassPathResource 訪問類載入路徑下的資源

ClassPathResource 用來訪問類載入路徑下的資源,相對於其他的 Resource 實現類,其主要優勢是方便訪問類載入路徑裡的資源,尤其對於 Web 應用,ClassPathResource 可自動搜尋位於 WEB-INF/classes 下的資原始檔,無須使用絕對路徑訪問。

public class ClassPathResourceTest {

    public static void loadAndReadUrlResource(String path) throws Exception{
        // 建立一個 Resource 物件,指定從檔案系統裡讀取資源,相對路徑
        ClassPathResource resource = new ClassPathResource(path);
        // 絕對路徑
//        UrlResource resource = new UrlResource("file:///Users/mr.l/test/CXuan-Spring/CXuan-Spring-Resource/books.xml");
        // 獲取檔名
        System.out.println("resource.getFileName = " + resource.getFilename());
        // 獲取檔案描述
        System.out.println("resource.getDescription = "+ resource.getDescription());
        SAXReader reader = new SAXReader();
        System.out.println(resource.getPath());
        Document document = reader.read("file:///Users/mr.l/test/CXuan-Spring/CXuan-Spring-Resource/books.xml");
        Element parent = document.getRootElement();
        List<Element> elements = parent.elements();
        for(Element element : elements){
            // 獲取name,description,price
            System.out.println(element.getName() + " = " +element.getText());
        }
    }


    public static void main(String[] args) throws Exception {
        loadAndReadUrlResource("books.xml");
    }
}

除了以上新建方式的不同,其他程式碼和上述程式碼一致,這就是 Spring 資源訪問的優勢:Spring 的資源訪問消除了底層資源訪問的差異,允許程式以一致的方式來訪問不同的底層資源。

使用FileSystemResource 訪問檔案資源系統

Spring 提供的 FileSystemResource 類用於訪問檔案系統資源,使用 FileSystemResource 來訪問檔案系統資源並沒有太大的優勢,因為 Java 提供的 File 類也可用於訪問檔案系統資源。

當然使用 FileSystemResource 也可消除底層資源訪問的差異,程式通過統一的 Resource API 來進行資源訪問。下面程式是使用 FileSystemResource 來訪問檔案系統資源的示例程式。

public class FileSystemResourceTest {

    public static void loadAndReadUrlResource(String path) throws Exception{
        // 建立一個 Resource 物件,指定從檔案系統裡讀取資源,相對路徑
        FileSystemResource resource = new FileSystemResource(path);
        // 絕對路徑
//        UrlResource resource = new UrlResource("file:///Users/mr.l/test/CXuan-Spring/CXuan-Spring-Resource/books.xml");
        // 獲取檔名
        System.out.println("resource.getFileName = " + resource.getFilename());
        // 獲取檔案描述
        System.out.println("resource.getDescription = "+ resource.getDescription());
        SAXReader reader = new SAXReader();
        System.out.println(resource.getFile());
        Document document = reader.read("file:///Users/mr.l/test/CXuan-Spring/CXuan-Spring-Resource/books.xml");
        Element parent = document.getRootElement();
        List<Element> elements = parent.elements();
        for(Element element : elements){
            // 獲取name,description,price
            System.out.println(element.getName() + " = " +element.getText());
        }
    }


    public static void main(String[] args) throws Exception {
        loadAndReadUrlResource("books.xml");
    }
}

FileSystemResource 例項可使用 FileSystemResource 構造器顯式地建立。但更多的時候它都是隱式建立的,執行 Spring 的某個方法時,該方法接受一個代表資源路徑的字串引數,當 Spring 識別該字串引數中包含 file: 字首後,系統將會自動建立 FileSystemResource 物件。

ServletContextResource

這是ServletContext資源的Resource實現,它解釋相關Web應用程式根目錄中的相對路徑。

它始終支援流(stream)訪問和URL訪問,但只有在擴充套件Web應用程式存檔且資源實際位於檔案系統上時才允許java.io.File訪問。無論它是在檔案系統上擴充套件還是直接從JAR或其他地方(如資料庫)訪問,實際上都依賴於Servlet容器。

InputStreamResource

InputStreamResource 是給定的輸入流(InputStream)的Resource實現。它的使用場景在沒有特定的資源實現的時候使用(感覺和@Component 的適用場景很相似)。

與其他Resource實現相比,這是已開啟資源的描述符。 因此,它的isOpen()方法返回true。如果需要將資源描述符保留在某處或者需要多次讀取流,請不要使用它。

ByteArrayResource

位元組陣列的Resource實現類。通過給定的陣列建立了一個ByteArrayInputStream。

它對於從任何給定的位元組陣列載入內容非常有用,而無需求助於單次使用的InputStreamResource。

Resource類圖與策略模式

上述Resource實現類與Resource頂級介面之間的關係可以用下面的UML關係模型來表示

策略模式

上述流程圖是不是對同一行為的不同實現方式,這種實現方式像極了策略模式?具體關於策略模式的文章,請參考

https://www.runoob.com/design-pattern/strategy-pattern.html

ResourceLoader 介面

ResourceLoader介面旨在由可以返回(即載入)Resource例項的物件實現,該介面實現類的例項將獲得一個 ResourceLoader 的引用。下面是ResourceLoader的定義

public interface ResourceLoader {
        
    //該介面僅包含這個方法,該方法用於返回一個 Resource 例項。ApplicationContext 的實現類都實現       ResourceLoader 介面,因此 ApplicationContext 可用於直接獲取 Resource 例項
    Resource getResource(String location);

}

所有的應用程式上下文都實現了ResourceLoader介面。因此,所有的應用程式上下文都可能會獲取Resource例項。

在特定應用程式上下文上呼叫getResource()並且指定的位置路徑沒有特定字首時,將返回適合該特定應用程式上下文的Resource型別。 例如,假設針對ClassPathXmlApplicationContext例項執行了以下程式碼:

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

你暫時不知道具體的上下文資源型別是什麼,假設指定的是ClassPathXmlApplicationContext,上述程式碼就會返回ClassPathResource,如果執行上面相同的方法的是FileSystemXmlApplicationContext,上述程式碼就會返回的是FileSystemResource,對於web系統來說,如果上下文容器時候WebApplicationContext,那麼返回的將是ServletContextResource,它同樣會為每個上下文返回適當的物件。因此,您可以以適合特定應用程式上下文的方式載入資源。

另一方面,你可能強制使用ClassPathResource,忽略應用程式的上下文型別,通過新增特定的字首classpath:,以下示例說明了這一點。

Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");

同樣的,你能夠強制使用UrlResource通過使用特定的字首:java.net.URL。下述兩個例子分別表示使用httpfile字首。

Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");

Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");

下列表格對資源型別和字首進行更好的彙總:

Prefix Example Explanation
classpath: classpath:com/myapp/config.xml 從類路徑載入
file: file:///data/config.xml 從檔案系統載入作為URL,查閱FileSystemResource
http: https://myserver/logo.png 載入作為URL
(none) /data/config.xml 依賴於ApplicationContext

ResourceLoaderAware 介面

這個ResourceLoaderAware介面是一個特殊的回撥介面,用於標識希望隨ResourceLoader引用提供的元件,下面是ResourceLoaderAware 介面的定義

public interface ResourceLoaderAware extends Aware {

    void setResourceLoader(ResourceLoader resourceLoader);
}

ResourceLoaderAware 介面用於指定該介面的實現類必須持有一個 ResourceLoader 例項。

類似於BeanNameAwareBeanFactoryAware介面,ResourceLoaderAware介面也提供了一個setResourceLoader()方法,該方法由Spring容器負責,Spring 容器會將一個 ResourceLoader 物件作為該方法的引數傳入。

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

在應用程式的元件中,除了實現 ResourceLoaderAware 介面,也可採取另外一種替代方案——依賴於 ResourceLoader 的自動裝配。傳統的建構函式注入和byType自動裝配模式(如自動裝配協作者中所述)能夠分別為建構函式引數或setter方法引數提供ResourceLoader。若為了獲得更大的靈活性(包括屬性注入的能力和多參方法),可以考慮使用基於註解的新注入方式。使用註解 @Autowiring 標記 ResourceLoader 變數,便可將其注入到成員屬性、構造引數或方法引數中。

使用Resource作為屬性

前面介紹了 Spring 提供的資源訪問策略,但這些依賴訪問策略要麼需要使用 Resource 實現類,要麼需要使用 ApplicationContext 來獲取資源。實際上,當應用程式中的 Bean 例項需要訪問資源時,Spring 有更好的解決方法:直接利用依賴注入。

從這個意義上來看,Spring 框架不僅充分利用了策略模式來簡化資源訪問,而且還將策略模式和 IoC 進行充分地結合,最大程度地簡化了 Spring 資源訪問。

歸納起來,如果 Bean 例項需要訪問資源,有如下兩種解決方案:

  • 程式碼中獲取 Resource 例項。
  • 使用依賴注入。

對於第一種方式的資源訪問,當程式獲取 Resource 例項時,總需要提供 Resource 所在的位置,不管通過 FileSystemResource 建立例項,還是通過 ClassPathResource 建立例項,或者通過 ApplicationContext 的 getResource() 方法獲取例項,都需要提供資源位置。這意味著:資源所在的物理位置將被耦合到程式碼中,如果資源位置發生改變,則必須改寫程式。因此,通常建議採用第二種方法,讓 Spring 為 Bean 例項依賴注入資源。

以下示例說明了這一點(可以使用set方法注入):

public class TestBean {

    private Resource resource;

    public Resource getResource() {
        return resource;
    }

    public void setResource(Resource resource) {
        this.resource = resource;
    }

    public void parse() throws Exception {
        // 獲取檔名
        System.out.println("resource.getFileName = " + resource.getFilename());
        // 獲取檔案描述
        System.out.println("resource.getDescription = "+ resource.getDescription());
        SAXReader reader = new SAXReader();
        Document document = reader.read("file:///Users/mr.l/test/CXuan-Spring/CXuan-Spring-Resource/books.xml");
        Element parent = document.getRootElement();
        List<Element> elements = parent.elements();
        for(Element element : elements){
            // 獲取name,description,price
            System.out.println(element.getName() + " = " +element.getText());
        }
    }

    public static void main(String[] args) throws Exception {
        TestBean testBean = new TestBean();
        testBean.setResource(new ClassPathResource("beans.xml"));
        testBean.parse();
    }
}

上面配置檔案配置了資源的位置,並使用了 classpath: 字首,這指明讓 Spring 從類載入路徑里加載 book.xml 檔案。與前面類似的是,此處的字首也可採用 http:、ftp: 等,這些字首將強制 Spring 採用怎樣的資源訪問策略(也就是指定具體使用哪個 Resource 實現類);如果不採用任何字首,則 Spring 將採用與該 ApplicationContext 相同的資源訪問策略來訪問資源。

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

應用程式上下文和資源路徑

本節介紹如何使用資源建立應用程式上下文,包括使用XML的快捷方式,如何使用萬用字元以及其他詳細資訊。

構造應用程式上下文

應用程式上下文建構函式(對於特定的應用程式上下文型別)通常將字串或字串陣列作為資源的位置路徑,例如構成上下文定義的XML檔案。

當這樣的位置路徑沒有字首時,從該路徑構建並用於載入bean定義的特定資源型別取決於並且適合於特定的應用程式上下文。 例如,請考慮以下示例,該示例建立ClassPathXmlApplicationContext:

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

bean 定義從類路徑中載入,因為ClassPathResource被使用了,然而,考慮以下例子,建立了一個FileSystemXmlApplicationContext:

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

現在bean的定義資訊會從檔案系統中載入,請注意,在位置路徑上使用特殊類路徑字首或標準URL字首會覆蓋為載入定義而建立的預設資源型別。 請考慮以下示例:

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

建立 Spring 容器時,系統將從類載入路徑來搜尋 appContext.xml;但使用 ApplicationContext 來訪問資源時,依然採用的是 FileSystemResource 實現類,這與 FileSystemXmlApplicationContext 的訪問策略是一致的。這表明:通過 classpath: 字首指定資源訪問策略僅僅對當次訪問有效,程式後面進行資源訪問時,還是會根據 AppliactionContext 的實現類來選擇對應的資源訪問策略。

應用程式上下文路徑中的萬用字元

上下文構造資源的路徑可能是一些簡單路徑,但是對於每一個對映來說,不可能只有簡單路徑,也會有特殊複雜的路徑出現,這就需要使用到路徑萬用字元(ant-style)。

ant-style示例

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

classpath* 和 classpath的區別:

classpath: 當使用 classpath :時字首來指定 XML 配置檔案時,系統將搜尋類載入路徑,找出所有與檔名的檔案,分別裝載檔案中的配置定義,最後合併成一個 ApplicationContext。

public static void main(String[] args) throws Exception { 
  // 使用 classpath* 裝載多份配置檔案輸出 ApplicationContext 例項。
  ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath*:bean.xml");
  System.out.println(ctx); 
}

如果不是採用 classpath*: 字首,而是改為使用 classpath: 字首,Spring 只加載第一份符合條件的 XML 檔案,例如如下程式碼

ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:bean.xml");

當使用 classpath: 字首時,系統通過類載入路徑搜尋 bean.xml 檔案,如果找到檔名匹配的檔案,系統立即停止搜尋,裝載該檔案,即使有多份檔名匹配的檔案,系統只裝載第一份檔案。

路徑匹配

另外,還有一種可以一次性裝載多份配置檔案的方式:指定配置檔案時指定使用萬用字元,例如如下程式碼:

ApplicationContext ctx = new ClassPathXmlApplicationContext("bean*.xml");

除此之外,Spring 甚至允許將 classpath*: 字首和萬用字元結合使用,如下語句也是合法的:

ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath*:bean*.xml");

file 字首的用法

相對路徑的寫法:

ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");

絕對路徑的寫法:

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

如果程式中需要訪問絕對路徑,則不要直接使用 FileSystemResource 或 FileSystemXmlApplicationContext 來指定絕對路徑。建議強制使用 file: 字首來區分相對路徑和絕對路徑,例如如下兩行程式碼

ApplicationContext ctx = new FileSystemXmlApplicationContext("file:bean.xml"); 
ApplicationContext ctx = new FileSystemXmlApplicationContext("file:/bean.xml");

文章參考:

Spring官方文件: https://docs.spring.io/spring/docs/5.1.7.RELEASE/spring-framework-reference/core.html#resources

IBM使用手冊:https://www.ibm.com/developerworks/cn/java/j-lo-spring-resource/index.h