天天看點

深入Spring IOC源碼之ResourceLoader

在《深入spring ioc源碼之resource》中已經詳細介紹了spring中resource的抽象,resource接口有很多實作類,我們當然可以使用各自的構造函數建立符合需求的resource執行個體,然而spring提供了resourceloader接口用于實作不同的resource加載政策,即将不同resource執行個體的建立交給resourceloader來計算。

public interface resourceloader {

    //classpath

    string classpath_url_prefix = resourceutils.classpath_url_prefix;

    resource getresource(string location);

    classloader getclassloader();

}

在resourceloader接口中,主要定義了一個方法:getresource(),它通過提供的資源location參數擷取resource執行個體,該執行個體可以是claspathresource、filesystemresource、urlresource等,但是該方法傳回的resource執行個體并不保證該resource一定是存在的,需要調用exists方法判斷。該方法需要支援一下模式的資源加載:

1.       url位置資源,如”file:c:/test.dat”

2.       classpath位置資源,如”classpath:test.dat”

3.       相對路徑資源,如”web-inf/test.dat”,此時傳回的resource執行個體根據實作不同而不同。

resourceloader接口還提供了getclassloader()方法,在加載classpath下的資源時作為參數傳入classpathresource。将classloader暴露出來,對于想要擷取resourceloader使用的classloader使用者來說,可以直接調用getclassloader()方法獲得,而不是依賴于thread context classloader,因為有些時候resourceloader内部使用自定義的classloader。

在實際開發中經常會遇到需要通過某種比對方式查找資源,而且可能有多個資源比對這種模式,在spring中提供了resourcepatternresolver接口用于實作這種需求,該接口繼承自resourceloader接口,定義了自己的模式比對接口:

public interface resourcepatternresolver extends resourceloader {

    string classpath_all_url_prefix = "classpath*:";

    resource[] getresources(string locationpattern) throws ioexception;

resourcepatternresolver定義了getresources()方法用于根據傳入的locationpattern查找和其比對的resource執行個體,并以數組的形式傳回,在傳回的數組中不可以存在相同的resource執行個體。resourcepatternresolver中還定義了”classpath*:”模式,用于表示查找classpath下所有的比對resource。

在spring中,對resourceloader提供了defaultresourceloader、filesystemresourceloader和servletcontextresourceloader等單獨實作,對resourcepatternresolver接口則提供了pathmatchingresourcepatternresolver實作。并且applicationcontext接口繼承了resourcepatternresolver,在實作中,applicationcontext的實作類會将邏輯代理給相關的單獨實作類,如pathmatchingresourceloader等。在applicationcontext中resourceloaderaware接口,可以将resourceloader(自身)注入到實作該接口的bean中,在bean中可以将其強制轉換成resourcepatternresolver接口使用(為了安全,強轉前需要判斷)。在spring中對resourceloader相關類的類圖如下:

深入Spring IOC源碼之ResourceLoader

defaultresourceloader是resourceloader的預設實作,abstractapplicationcontext繼承該類(關于這個繼承,簡單吐槽一下,spring内部感覺有很多這種個人感覺使用組合更合适的繼承,比如還有abstractbeanfactory繼承自factorybeanregisterysupport,這個讓我看起來有點不習慣,而且也增加了類的繼承關系)。它接收classloader作為構造函數的參數,或使用不帶參數的構造函數,此時classloader使用預設的classloader(一般為thread context classloader),classloader也可以通過set方法後繼設定。

其最主要的邏輯實作在getresource方法中,該方法首先判斷傳入的location是否以”classpath:”開頭,如果是,則建立classpathresource(移除”classpath:”字首),否則嘗試建立urlresource,如果目前location沒有定義url的協定(即以”file:”、”zip:”等開頭,比如使用相對路徑”resources/meta-inf/menifest.mf),則建立urlresource會抛出malformedurlexception,此時調用getresourcebypath()方法擷取resource執行個體。getresourcebypath()方法預設傳回classpathcontextresource執行個體,在filesystemresourceloader中有不同實作。

public resource getresource(string location) {

    assert.notnull(location, "location must not be null");

    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 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());

filesystemresourceloader繼承自defaultresourceloader,它的getresource方法的實作邏輯和defaultresourceloader相同,不同的是它實作了自己的getresourcebypath方法,即當urlresource建立失敗時,它會使用filesystemcontextresource執行個體而不是classpathcontextresource:

    if (path != null && path.startswith("/")) {

        path = path.substring(1);

    return new filesystemcontextresource(path);

使用該類時要特别注意的一點:即使location以”/”開頭,資源的查找還是相對于vm啟動時的相對路徑而不是絕對路徑(從以上代碼片段也可以看出,它會先截去開頭的”/”),這個和servlet container保持一緻。如果需要使用絕對路徑,需要添加”file:”字首。

servletcontextresourceloader類繼承自defaultresourceloader,和filesystemresourceloader一樣,它的getresource方法的實作邏輯和defaultresourceloader相同,不同的是它實作了自己的getresourcebypath方法,即當urlresource建立失敗時,它會使用servletcontextresource執行個體:

    return new servletcontextresource(this.servletcontext, path);

這裡的path即使以”/”開頭,也是相對servletcontext的路徑,而不是絕對路徑,要使用絕對路徑,需要添加”file:”字首。

pathmatchingresourcepatternresolver類實作了resourcepatternresolver接口,它包含了對resourceloader接口的引用,在對繼承自resourceloader接口的方法的實作會代理給該引用,同時在getresources()方法實作中,當找到一個比對的資源location時,可以使用該引用解析成resource執行個體。預設使用defaultresourceloader類,使用者可以使用構造函數傳入自定義的resourceloader。

pathmatchingresourcepatternresolver還包含了一個對pathmatcher接口的引用,該接口基于路徑字元串實作比對處理,如判斷一個路徑字元串是否包含通配符(’*’、’?’),判斷給定的path是否比對給定的pattern等。spring提供了antpathmatcher對pathmatcher的預設實作,表達該pathmatcher是采用ant風格的實作。其中pathmatcher的接口定義如下:

public interface pathmatcher {

    boolean ispattern(string path);

    boolean match(string pattern, string path);

    boolean matchstart(string pattern, string path);

    string extractpathwithinpattern(string pattern, string path);

ispattern(string path):

判斷path是否是一個pattern,即判斷path是否包含通配符:

public boolean ispattern(string path) {

    return (path.indexof('*') != -1 || path.indexof('?') != -1);

match(string pattern, string path):

判斷給定path是否可以比對給定pattern:

matchstart(string pattern, string path):

判斷給定path是否可以比對給定pattern,該方法不同于match,它隻是做部分比對,即當發現給定path比對給定path的可能性比較大時,即傳回true。在pathmatchingresourcepatternresolver中,可以先使用它确定需要全面搜尋的範圍,然後在這個比較小的範圍内再找出所有的資源檔案全路徑做比對運算。

在antpathmatcher中,都使用domatch方法實作,match方法的fullmatch為true,而matchstart的fullmatch為false:

protected boolean domatch(string pattern, string path, boolean fullmatch)

domatch的基本算法如下:

1.       檢查pattern和path是否都以”/”開頭或者都不是以”/”開頭,否則,傳回false。

2.       将pattern和path都以”/”為分隔符,分割成兩個字元串數組pattarray和patharray。

3.       從頭周遊兩個字元串數組,如果遇到兩給字元串不比對(兩個字元串的比對算法再下面介紹),傳回false,否則,直到遇到pattarray中的”**”字元串,或pattarray和patharray中有一個周遊完。

4.       如果pattarray周遊完:

a)         patharray也周遊完,并且pattern和path都以”/”結尾或都不以”/”,傳回true,否則傳回false。

b)         pattarray沒有周遊完,但fullmatch為false,傳回true。

c)         pattarray隻剩最後一個”*”,同時path以”/”結尾,傳回true。

d)         pattarray剩下的字元串都是”**”,傳回true,否則傳回false。

5.       如果patharray沒有周遊完,而pattarray周遊完了,傳回false。

6.       如果patharray和pattarray都沒有周遊完,fullmatch為false,而且pattarray下一個字元串為”**”時,傳回true。

7.       從後開始周遊patharray和pattarray,如果遇到兩個字元串不比對,傳回false,否則,直到遇到pattarray中的”**”字元串,或patharray和pattarray中有一個和之前的周遊索引相遇。

8.       如果是因為patharray與之前的周遊索引相遇,此時,如果沒有周遊完的pattarray所有字元串都是”**”,則傳回true,否則,傳回false。

9.       如果patharray和pattarray中間都沒有周遊完:

a)         去除pattarray中相鄰的”**”字元串,并找到其下一個”**”字元串,其索引号為pattidxtmp,他們的距離即為s

b)         從剩下的patharray中的第i個元素向後查找s個元素,如果找到所有s個元素都比對,則這次查找成功,記i為temp,如果沒有找到這樣的s個元素,傳回false。

c)         将pattarray的起始索引設定為pattidxtmp,将patharray的索引号設定為temp+s,繼續查找,直到pattarray或patharray周遊完。

10.   如果pattarray沒有周遊完,但剩下的元素都是”**”,傳回true,否則傳回false。

對路徑字元串數組中的字元串比對算法如下:

1.       記pattern為模式字元串,str為要比對的字元串,将兩個字元串轉換成兩個字元數組pattarray和strarray。

2.       周遊pattarray直到遇到’*’字元。

3.       如果pattarray中不存在’*’字元,則隻有在pattarray和strarray的長度相同兩個字元數組中所有元素都相同,其中pattarray中的’?’字元可以比對strarray中的任何一個字元,否則,傳回false。

4.       如果pattarray隻包含一個’*’字元,傳回true

5.       周遊pattarray和strarray直到pattarray遇到’*’字元或strarray周遊完,如果存在不比對的字元,傳回false。

6.       如果因為strarray周遊完成,而pattarray剩下的字元都是’*’,傳回true,否則傳回false

7.       從末尾開始周遊pattarray和strarray,直到pattarray遇到’*’字元,或strarray遇到之前的周遊索引,中間如果遇到不比對字元,傳回false

8.       如果strarray周遊完,而剩下的pattarray字元都是’*’字元,傳回true,否則傳回false

9.       如果pattarray和strarray都沒有周遊完(類似之前的算法):

a)         去除pattarray相鄰的’*’字元,查找下一個’*’字元,記其索引号為pattidxtmp,兩個’*’字元的相隔距離為s

b)         從剩下的strarray中的第i個元素向後查找s個元素,如果有找到所有s個元素都比對,則這次查找成功,記i為temp,如果沒有到這樣的s個元素,傳回false。

c)         将pattarray的起始索引設定為pattidxtmp,strarray的起始索引設定為temp+s,繼續查找,直到pattarray或strarray周遊完。

10.   如果pattarray沒有周遊完,但剩下的元素都是’*’,傳回true,否則傳回false

string extractpathwithinpattern(string pattern, string path):

去除path中和pattern相同的字元串,隻保留比對的字元串。比如如果pattern為”/doc/csv/*.htm”,而path為”/doc/csv/commit.htm”,則該方法的傳回值為commit.htm。該方法預設pattern和path已經比對成功,因而算法比較簡單:

以’/’分割pattern和path為兩個字元串數組pattarray和patharray,周遊pattarray,如果該字元串包含’*’或’?’字元,則并且patharray的長度大于目前索引号,則将該字元串添加到結果中。

周遊完pattarray後,如果patharray長度大于pattarray,則将剩下的patharray都添加到結果字元串中。

最後傳回該字元串。

不過也正是因為該算法實作比較簡單,因而它的結果貌似不那麼準确,比如pattern的值為:/com/**/levin/**/commit.html,而path的值為:/com/citi/cva/levin/html/commit.html,其傳回結果為:citi/levin/commit.html

現在言歸正傳,看一下pathmatchingresourcepatternresolver中的getresources方法的實作:

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()));

        // only look for a pattern after a prefix here

        // (to not get fooled by a pattern symbol in a strange prefix).

        int prefixend = locationpattern.indexof(":") + 1;

        if (getpathmatcher().ispattern(locationpattern.substring(prefixend))) {

            // a file pattern

            // a single resource with the given name

            return new resource[] {getresourceloader().getresource(locationpattern)};

對classpath下的資源,相同名字的資源可能存在多個,如果使用”classpath*:”作為字首,表明需要找到classpath下所有該名字資源,因而需要調用findclasspathresources方法查找classpath下所有該名稱的resource,對非classpath下的資源,對于不存在模式字元的location,一般認為一個location對應一個資源,因而直接調用resourceloader.getresource()方法即可(對classpath下沒有以”classpath*:”開頭的location也适用)。

findclasspathresources方法實作相對比較簡單:

适用classloader.getresources()方法,周遊結果url集合,将每個結果适用urlresource封裝,最後組成一個resource數組傳回即可。

對包含模式比對字元的location來說,需要調用findpathmatchingresources方法:

protected resource[] findpathmatchingresources(string locationpattern) throws ioexception {

    string rootdirpath = determinerootdir(locationpattern);

    string subpattern = locationpattern.substring(rootdirpath.length());

    resource[] rootdirresources = getresources(rootdirpath);

    set result = new linkedhashset(16);

    for (int i = 0; i < rootdirresources.length; i++) {

        resource rootdirresource = resolverootdirresource(rootdirresources[i]);

        if (isjarresource(rootdirresource)) {

            result.addall(dofindpathmatchingjarresources(rootdirresource, subpattern));

            result.addall(dofindpathmatchingfileresources(rootdirresource, subpattern));

    if (logger.isdebugenabled()) {

        logger.debug("resolved location pattern [" + locationpattern + "] to resources " + result);

    return (resource[]) result.toarray(new resource[result.size()]);

1.       determinrootdir()方法傳回locationpattern中最長的沒有出現模式比對字元的路徑

2.       subpattern則表示rootdirpath之後的包含模式比對字元的路徑信pattern

3.       使用getresources()擷取rootdirpath下的所有資源數組。

4.       周遊這個數組。

a)         對jar中的資源,使用dofindpathmatchingjarresources()方法來查找和比對。

b)         對非jar中資源,使用dofindpathmatchingfileresources()方法來查找和比對。

dofindpathmatchingjarresources()實作:

1.       計算目前resource在jar檔案中的根路徑rootentrypath。

2.       周遊jar檔案中所有entry,如果目前entry名以rootentrypath開頭,并且之後的路徑資訊和之前從patternlocation中截取出的subpattern使用pathmatcher比對,若比對成功,則調用rootdirresource.createrelative方法建立一個resource,将新建立的resource添加入結果集中。

dofindpathmatchingfileresources()實作:

1.       擷取要查找資源的根路徑(根路徑全名)

2.       遞歸獲得根路徑下的所有資源,使用pathmatcher比對,如果比對成功,則建立filesystemresource,并将其加入到結果集中。在遞歸進入一個目錄前首先調用pathmatcher.matchstart()方法,以先簡單的判斷是否需要遞歸進去,以提升性能。

protected void doretrievematchingfiles(string fullpattern, file dir, set result) throws ioexception {

        logger.debug("searching directory [" + dir.getabsolutepath() +

                "] for files matching pattern [" + fullpattern + "]");

    file[] dircontents = dir.listfiles();

    if (dircontents == null) {

        throw new ioexception("could not retrieve contents of directory [" + dir.getabsolutepath() + "]");

    for (int i = 0; i < dircontents.length; i++) {

        file content = dircontents[i];

        string currpath = stringutils.replace(content.getabsolutepath(), file.separator, "/");

        if (content.isdirectory() && getpathmatcher().matchstart(fullpattern, currpath + "/")) {

            doretrievematchingfiles(fullpattern, content, result);

        if (getpathmatcher().match(fullpattern, currpath)) {

            result.add(content);

最後,需要注意的是,由于classloader.getresources()方法存在的限制,當傳入一個空字元串時,它隻能從classpath的檔案目錄下查找,而不會從jar檔案的根目錄下查找,因而對”classpath*:”字首的資源來說,找不到jar根路徑下的資源。即如果我們有以下定義:”classpath*:*.xml”,如果隻有在jar檔案的根目錄下存在*.xml檔案,那麼這個pattern将傳回空的resource數組。解決方法是不要再jar檔案根目錄中放檔案,可以将這些檔案放到jar檔案中的resources、config等目錄下去。并且也不要在”classpath*:”之後加一些通配符,如”classpath*:**/*enum.class”,至少在”classpath*:”後加入一個不存在通配符的路徑名。

servletcontextresourcepatternresolver類繼承自pathmatchingresourcepatternresolver類,它重寫了父類的檔案查找邏輯,即對servletcontextresource資源使用servletcontext.getresourcepaths()方法來查找參數目錄下的檔案,而不是file.listfiles()方法:

protected set dofindpathmatchingfileresources(resource rootdirresource, string subpattern) throws ioexception {

    if (rootdirresource instanceof servletcontextresource) {

        servletcontextresource scresource = (servletcontextresource) rootdirresource;

        servletcontext sc = scresource.getservletcontext();

        string fullpattern = scresource.getpath() + subpattern;

        set result = new linkedhashset(8);

        doretrievematchingservletcontextresources(sc, fullpattern, scresource.getpath(), result);

        return result;

        return super.dofindpathmatchingfileresources(rootdirresource, subpattern);

在abstractapplicationcontext中,對resourcepatternresolver的實作隻是簡單的将getresources()方法的實作代理給resourcepatternresolver字段,而該字段預設在abstractapplicationcontext建立時建立一個pathmatchingresourcepatternresolver執行個體:

public abstractapplicationcontext(applicationcontext parent) {

    this.parent = parent;

    this.resourcepatternresolver = getresourcepatternresolver();

protected resourcepatternresolver getresourcepatternresolver() {

    return new pathmatchingresourcepatternresolver(this);

    return this.resourcepatternresolver.getresources(locationpattern);