1. 程式人生 > >Spring源碼系列 — Resource抽象

Spring源碼系列 — Resource抽象

spring文檔 sort plain external 技術分享 簡單 ava system cache

前言

前面兩篇介紹了上下文的啟動流程和Environemnt的初始化,這兩部分都是屬於上下文自身屬性的初始化。這篇開始進入Spring如何加載實例化Bean的部分 — 資源抽象與加載。

本文主要從以下方面介紹Spring中的資源Resource:

  • 前提準備
  • Resource抽象
  • Resource加載解析
  • 何時何地觸發加載解析
  • 總結

前提準備

Spring中的資源抽象使用到了很多陌生的api,雖然這些api都是JDK提供,但是日常的業務場景中很少使用或者接觸不深。在閱讀Resource的源碼前,需要完善知識體系,減輕閱讀Resource實現的難度。

1.URL和URI

URL代表Uniform Resource Locator,指向萬維網上的一個資源。資源可以是文件、聲音、視頻等。
URI代表Uniform Resource Identifier,用於標識一個特定資源。URL是URI的一種,也可以用於標識資源。

URI的語法如下:

技術分享圖片

不在第一條直線上的部分都是可選。關於更多URL和URI的詳細信息可以參考URL和URI

在Java中提供了兩個類分別表示URL和URI。兩者在描述資源方面提供了很多相同的屬性:protocol(scheme)/host/port/file(path)等等。但是URL除此還提供了建立Tcp連接,獲取Stream的操作。如:

public URLConnection openConnection() throws java.io.IOException {
    return handler.openConnection(this);
}

public final InputStream openStream() throws java.io.IOException {
    return openConnection().getInputStream();
}

因為URL表示網絡中的資源路徑,所以它能夠提供網絡操作獲取資源。Spring中包含UrlResource即是對URL的封裝,提供獲取資源的便捷性。

2.Class和ClassLoader

Class是Java中對象類型。Class對象提供了加載資源的能力,根據資源名稱搜索,返回資源的URL:

public java.net.URL getResource(String name) {
    ...省略
}

ClassLoader是類加載器,它同樣也提供了加載資源的能力:

// 搜索單個資源
public URL getResource(String name) {
    ...省略
}

// 搜索匹配的多個資源
public Enumeration<URL> getResources(String name) throws IOException {
    ...省略
}

Class中getResource委托加載該Class的ClassLoader搜索匹配的資源。搜索規則:委托父類加載器搜索,父類加載器為空再有啟動類加載器搜索。搜索結果為空,再由該類加載器的findResources搜索。

在Spring中,加載類路徑上的資源就由ClassLoader.getResources完成。

3.URLClassLoader

URLClassLoader是Java中用於從搜索路徑上加載類和資源,搜索路徑包括JAR文件和目錄。
在Spring中利用其獲得所有的搜索路徑—即JAR files和目錄,然後從目錄和JAR files中搜索匹配特定的資源。如:
Spring支持Ant風格的匹配,當搜索模式*.xml的資源時,無法通過classLoader.getResources獲取,故Spring自實現獲取匹配該模式的資源。

URLClassLoader提供接口獲取所有的搜索路徑:

/**
 * Returns the search path of URLs for loading classes and resources.
 * This includes the original list of URLs specified to the constructor,
 * along with any URLs subsequently appended by the addURL() method.
 * @return the search path of URLs for loading classes and resources.
 */
public URL[] getURLs() {
    return ucp.getURLs();
}
3.JarFile和JarEntry

對於JarFile和JarEntry類,筆者自己也未曾在工作中使用過。不過從命名上也可以看出一些貓膩。
JarFile用於表示一個Jar,可以利用其api讀取Jar中的內容。
JarEntry用於表示Jar文件中的條目,比如:org/springframework/context/ApplicationContext.class

通過JarFile提供的entries接口可以獲取jar文件中所有的條目:

public Enumeration<JarEntry> entries() {
    return new JarEntryIterator();
}

通過遍歷條目,可以達到從Jar文件中檢索需要的條目,即class。

4.JarURLConnection

同上,筆者之前也曾為接觸JarURLConnection。JarURLConnection是URLConnection的實現類,表示對jar文件或者jar文件中的條目的URL連接。提供了獲取輸入流的能力,例外還提供獲取JarFile對象的api。

語法如下:

jar:<url>!/{entry}

jar表示文件類型為jar,url表示jar文件位置,"!/"表示分隔符。

例如:

  • jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class表示網絡上的jar文件的Quux類;
  • jar:file:/home/duke/duke.jar!/表示文件系統中的duke.jar包。
// jar條目
jar:http://www.foo.com/bar/baz.jar!/COM/foo/Quux.class

// jar文件
jar:http://www.foo.com/bar/baz.jar!/

// jar目錄
jar:http://www.foo.com/bar/baz.jar!/COM/foo/

JarURLConnection提供獲取JarFile和JarEntry對象的api:

public abstract JarFile getJarFile() throws IOException;

public JarEntry getJarEntry() throws IOException {
    return getJarFile().getJarEntry(entryName);
}

Tips
在Spring的Resource實現中,主要使用到了這些平時很少使用的陌生api。在閱讀Resource實現前,非常有必要熟悉這些api。

Resource抽象

在Spring中的資源的抽象非常復雜,根據資源位置的不同,分別實現了紛繁復雜的資源。整體Resource的UML類圖如下:

技術分享圖片

Note:
Spring中Resource模塊使用了策略模式,上層實現統一的資源抽象接口,針對不同的Resource類型,分別封裝各自的實現,然後在相應的場景中組合使用相應的資源類型。其中對於多態、繼承的應用可謂淋漓盡致。

從以上的UML類圖中可以看出:

  1. 將輸入流作為源頭的對象抽象為接口,可以表示輸入流源;
  2. Spring統一抽象Resource接口,表示應用中的資源,如文件或者類路徑上的資源。Resource繼承上述的輸入流源,則Resource抽象具有獲取資源內容的能力;
  3. 在上圖的下部,分別是Resource接口的具體實現。根據資源的表示方式不同,分為:
    文件系統Resource、字節數組Resource、URL的Resource、類路勁上的Resource等等;
1.UrlResource

UrlResource通過包裝URL對象達到方位目標資源,目標資源包括:file、http網絡資源、ftp文件資源等等。URL協議類型:"file:"文件系統、"http:"http協議、"ftp:"ftp協議。

pulic class UrlResource extends AbstractFileResolvingResource {
   private final URI uri;

   // 代表資源位置的url
   private final URL url;

    // 通過uri構造UrlResource對象
   public UrlResource(URI uri) throws MalformedURLException {
       Assert.notNull(uri, "URI must not be null");
       this.uri = uri;
       this.url = uri.toURL();
       this.cleanedUrl = getCleanedUrl(this.url, uri.toString());
   }
   // 通過url構造UrlResource對象
   public UrlResource(URL url) {
       Assert.notNull(url, "URL must not be null");
       this.url = url;
       this.cleanedUrl = getCleanedUrl(this.url, url.toString());
       this.uri = null;
   }
   // 通過path路徑構造UrlResource對象
   public UrlResource(String path) throws MalformedURLException {
       Assert.notNull(path, "Path must not be null");
       this.uri = null;
       this.url = new URL(path);
       this.cleanedUrl = getCleanedUrl(this.url, path);
   }

   ...省略
}
2.ClassPathResource

ClassPathResource代表類路徑資源,該資源可以由類加載加載。如果該資源在文件系統中,則支持使用getFile接口獲取該資源對應的File對象;如果該資源在jar文件中但是無法擴展至文件系統中,則不支持解析為File對象,此時可以使用URL加載。

public class ClassPathResource extends AbstractFileResolvingResource {
    // 文件在相對於類路徑的路徑
    private final String path;
    // 指定類加載器
    private ClassLoader classLoader;
    private Class<?> clazz;

    public ClassPathResource(String path) {
        this(path, (ClassLoader) null);
    }
    public ClassPathResource(String path, ClassLoader classLoader) {
        Assert.notNull(path, "Path must not be null");
        String pathToUse = StringUtils.cleanPath(path);
        if (pathToUse.startsWith("/")) {
            pathToUse = pathToUse.substring(1);
        }
        this.path = pathToUse;
        this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
    }
}
3.FileSystemResource

FileSystemResource代表文件系統上的資源。可以通過getFile和getURL接口獲取對應的File和URL對象。

public class FileSystemResource extends AbstractResource implements WritableResource {
    // 代表資源的File對象
    private final File file;
    // 文件系統路徑
    private final String path;

    public FileSystemResource(File file) {
        Assert.notNull(file, "File must not be null");
        this.file = file;
        this.path = StringUtils.cleanPath(file.getPath());
    }
    public FileSystemResource(String path) {
        Assert.notNull(path, "Path must not be null");
        this.file = new File(path);
        this.path = StringUtils.cleanPath(path);
    }
}
4.ByteArrayResource

ByteArrayResource將字節數組byte[]包裝成Resource。

5.InputStreamResource

InputStreamResource將輸入流InputStream包裝成Resource對象。

Spring文檔Tips:
雖然Resouce為Spring框架設計和被Spring框架內部大量使用。但是Resource還可以作為通用的工具模塊使用,日常的應用開發過程中涉及到資源的處理,推薦使用Resource抽象,因為Resource提供了操作資源的便捷接口,可以簡化對資源的操作。雖然耦合Spring,但是在Spring產品的趨勢下,還會有耦合?

Resource加載解析

Resource加載是基於XML配置Spring上下文的核心基礎模塊。Spring提供了強大的加載資源的能力。可以分為兩種模式:

  • 根據簡單的資源路徑,加載資源;
  • 根據復雜的的資源路徑:Ant-Style、classpath:、classpath*:等等,解析如此復雜的資源路徑,加載資源;

Spring依次抽象出ResourceLoader和ResourcePatternResolver兩部分實現以上的兩種情況:

  • ResourceLoader純粹的加載資源;
  • ResourcePatternResolver負責解析復雜多樣的資源路徑並加載資源,它本身也是加載器的一種,實現ResourceLoader接口;
技術分享圖片

Note:
策略模式是一個傳神的模式,在Spring中最讓我感嘆的兩個模式之一,在Spring中隨處可見,可能源於策略模式是真的易擴展而帶來的隨心所欲的應對各種場景帶來的效果吧。這裏定義策略接口ResourceLoader,根據不同的場景實現相應的資源加載器,是典型策略的應用方式。

Spring對加載資源定義頂層接口ResourecLoader:

public interface ResourceLoader {

    /** Pseudo URL prefix for loading from the class path: "classpath:" */
    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;

     // 根據資源路徑加載resource對象
    Resource getResource(String location);
    // ResourceLoader是利用java的類加載器實現資源的加載
    ClassLoader getClassLoader();
}

該接口是加載資源的策略接口,Spring中加載資源模塊的最頂層定義。Spring應用需要配置各種各樣的配置,這些決定上下文具有加載資源的能力。在Spring中所有上下文都繼承了ResouceLoader接口,加載資源是Spring上下文的基本能力。

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
        MessageSource, ApplicationEventPublisher, ResourcePatternResolver {

統一的上下文接口繼承了ResoucePatternResolver接口,間接繼承了ResourceLoader。

從ResouceLoader的接口定義上也可以看出,ResourceLoader只能加載單個資源,功能比較簡單。其有個默認實現DefaultResourceLoader,在看DefaultResourceLoader之前,首先了解DefaultResourceLoader的SPI。

Spring提供了ProtocolResolver策略接口,也是策略模式的應用,為了解決特定協議的資源解析。被用於DefaultResourceLoader,使其解析資源的能力得以擴展。

public interface ProtocolResolver {
     // 根據特定路徑解析資源
    Resource resolve(String location, ResourceLoader resourceLoader);
}

應用可以自實現該接口,將其實現加入,如:

/**
 * 實現對http協議URL資源的解析
 *
 * @author huaijin
 */
public class FromHttpProtocolResolver implements ProtocolResolver {

    @Override
    public Resource resolve(String location, ResourceLoader resourceLoader) {
        if (location == null || location.isEmpty() || location.startsWith("http://")) {
            return null;
        }
        byte[] byteArray;
        InputStream inputStream = null;
        try {
            URL url = new URL(location);
            inputStream = url.openStream();
            byteArray = StreamUtils.copyToByteArray(inputStream);
        } catch (MalformedURLException e) {
            throw new RuntimeException("location isn‘t legal url.", e);
        } catch (IOException e) {
            throw new RuntimeException("w/r err.", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return new ByteArrayResource(byteArray);
    }
}


// 將其加入上下文容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
context.addProtocolResolver(new FromHttpProtocolResolver());

DefaultResouceLoader中部分源代碼如下:

public class DefaultResourceLoader implements ResourceLoader {

    // 類加載器,可以編程式設置
    private ClassLoader classLoader;

    // 協議解析器集合
    private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<ProtocolResolver>(4);

    // 增加協議解析器
    public void addProtocolResolver(ProtocolResolver resolver) {
        Assert.notNull(resolver, "ProtocolResolver must not be null");
        this.protocolResolvers.add(resolver);
    }


    // 加載單個資源實現
    @Override
    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;
            }
        }

        // 資源路徑以"/"開頭,表示是類路徑上下文資源ClasspathContextResource
        if (location.startsWith("/")) {
            return getResourceByPath(location);
        }
        // 資源以"classpath:"開頭,表示是類路徑資源ClasspathResource
        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }
        else {
            // 否則認為是路徑時URL,嘗試作為URL解析
            try {
                // Try to parse the location as a URL...
                URL url = new URL(location);
                return new UrlResource(url);
            }
            catch (MalformedURLException ex) {
                // 如果不是URL,則再次作為類路徑上下文資源ClasspathContextResource
                // No URL -> resolve as resource path.
                return getResourceByPath(location);
            }
        }
    }


    protected Resource getResourceByPath(String path) {
        return new ClassPathContextResource(path, getClassLoader());
    }
}

Note:
DefaultResourceLoader實現整體比較簡單,但是值得借鑒的是使用ProtocolResolver擴展機制,可以認為是預留鉤子。
所有ApplicationContext都繼承了ResourceLoader接口從而具有了資源加載的基本能力,但是對於ApplicationContext都去主動實現該接口無疑使ApplicationContext強耦合資源加載能力,不易加載能力的擴展。Spring這裏的設計非常精妙,遵循接口隔離原則。資源加載能力單獨隔離成ResourceLoader接口,使其獨立演變。ApplicationContext通過繼承該接口而具有資源加載能力,ApplicationContext的實現中再繼承或者組合ResourceLoader的實現DefaultResourceLoader。這樣ResourceLoader可以自由擴展,而不影響ApplicationContext。當然Spring這裏采用了AbstractApplicationContext繼承DefaultResourceLoader。

Tips:
ResourceLoader雖然在Spring框架大量應用,但是ResourceLoader可以作為工具使用,可以極大簡化代碼。強力推薦應用中加載資源時使用ResourceLoader,應用可以通過繼承ResouceLoaderAware接口或者@Autowired註入ResouceLoader。當然也可以通過ApplicationContextAware獲取ApplicationContext作為ResouceLoader使用,但是如此,無疑擴大接口範圍,有違封裝性。詳細參考:https://docs.spring.io/spring/docs/4.3.20.RELEASE/spring-framework-reference/htmlsingle/#resources

Spring另外一種資源加載方式ResourcePatternResolver是Spring中資源加載的核心。ResourcePatternResolver本身也是ResourceLoader的接口擴張。其特點:

  • 支持解析復雜化的資源路徑
  • 加載多資源
public interface ResourcePatternResolver extends ResourceLoader {

    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    Resource[] getResources(String locationPattern) throws IOException;
}

getResources的定義決定了ResourcePatternResolver具有根據資源路徑模式locationPattern加載多資源的能力。
並且提供了新的模式:在所有的類路徑上"classpath*:"。

Note:
這裏又使用到了策略模式,ResourcePatternResolver是策略接口,可以根據不同的路徑模式封裝實現相應的實現。有沒有感覺到策略模式無處不在!

頂層上下文容器ApplicationContext通過繼承ResourcePatternResolver使其具有按照模式解析加載資源的能力。這裏不再贅述,前文接受ResourceLoader時有所描述。

ResourcePatternResolver的路徑模式非常多,首先這是不確定的。根據不同的模式,有相應的實現。PathMatchingResourcePatternResolver是其標準實現。
在深入PathMatchingResourcePatternResolver的源碼前,先了解下Ant—Style,因為PathMatchingResourcePatternResolver是ResourcePatternResolver在支持Ant-Style模式和classpath*模式的實現:

  1. "*"代表一個或者多個字符,如模式beans-*.xml可以匹配beans-xxy.xml;
  2. "?"代表單個字符,如模式beans-?.xml可以匹配beans-1.xml;
  3. "**"代表任意路徑,如模式/**/bak.txt可以匹配/xxx/yyy/bak.txt;

在Spring中有Ant-Sytle匹配器AntPathMatcher。該匹配實現接口PathMatcher:

public interface PathMatcher {

     // 判斷給定模式路徑是否為指定模式
    boolean isPattern(String path);

     // 判斷指定模式是否能匹配指定路徑path
    boolean match(String pattern, String path);
}

PathMatcher是一個策略接口,表示路徑匹配器,有默認Ant-Style匹配器實現AntPathMatcher。

Note:
這裏仍然使用策略模式。組合是使用策略模式的前提!

再回到PathMatchingResourcePatternResolver,其支持:

  • 解析Ant-Style路徑;
  • 解析classpath*模式路徑;
  • 解析classpath*和Ant-Style結合的路徑;
  • 加載以上解析出來的路徑上的資源;

PathMatchingResourcePatternResolver中持有AntPathMatcher實現對Ant-Style的解析,持有ResourceLoader實現對classpath*的解析。PathMatchingResourcePatternResolver中成員持有關系:

public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
    // 持有resouceLoader
    private final ResourceLoader resourceLoader;
    // 持有AntPathMatcher
    private PathMatcher pathMatcher = new AntPathMatcher();

    public PathMatchingResourcePatternResolver() {
        this.resourceLoader = new DefaultResourceLoader();
    }
    // 指定ResourceLoader構造
    public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
        Assert.notNull(resourceLoader, "ResourceLoader must not be null");
        this.resourceLoader = resourceLoader;
    }
}

在PathMatchingResourcePatternResolver中核心的方法數ResourcePatternResolver中定義的getResources的實現,其負責加載多樣模式路徑上的資源:

@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)) {
        // 路徑模式是否為指定模式,這裏指定模式是Ant-Style,即判斷路徑模式是否為Ant-Style
        // 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
            // 1-1.如果是Ant-Style,則在所有的類路徑上查找匹配該模式的資源
            return findPathMatchingResources(locationPattern);
        }
        else {
            // 1-2.如果不是Ant-Style,則在所有類路徑上查找精確匹配該名稱的的資源
            // all class path resources with the given name
            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    }
    // 2.不是以classpath*開頭
    else {
        // Generally only look for a pattern after a prefix here,
        // and on Tomcat only after the "*/" separator for its "war:" protocol.
        // tomcat的war協議比較特殊,路徑模式在war協議的*/後面,需要截取*/的路徑模式
        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                locationPattern.indexOf(‘:‘) + 1);
        // 判斷路徑模式是否為指定的模式,這裏是Ant-Style,即判斷路徑模式是否為Ant-Style
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            // a file pattern
            // 2-1.是指定的路徑模式,根據模式查找匹配的資源
            return findPathMatchingResources(locationPattern);
        }
        // 2-2.如果不是Ant-Style,則認為是單個資源路徑,使用ResourceLoader加載單個資源
        else {
            // a single resource with the given name
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

這裏加載資源的邏輯根據路徑模式的不同,分支情況非常多,邏輯也非常復雜。為了更加詳細而清晰的探索,這裏分別深入每種情況,並為每種情況舉例相應的資源路徑模式。

1.1-1分支(類路徑資源模式)

1-1分支進入條件需要滿足:

  • 路徑以classpath*:開頭;
  • 路徑時Ant-Style風格,即路徑中包含通配符;

如:classpath*:/META-INF/bean-*.xml和classpath*:*.xml都會進入1-1分支。

// 根據給定模式通過ant風格匹配器尋找所有匹配的資源,locationPattern為路徑模式。
// 支持從文件系統、jar、zip中尋找資源
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    // 截取根路徑,即取ant通配符之前的路徑部分
    String rootDirPath = determineRootDir(locationPattern);
    // 從通配符位置開始,截取路徑的後續部分(子模式)
    String subPattern = locationPattern.substring(rootDirPath.length());
    // 從根路徑部分獲取所有的資源
    Resource[] rootDirResources = getResources(rootDirPath);
    // 在spring中對於集合使用有個良好習慣,初始化時始終指定集合大小
    Set<Resource> result = new LinkedHashSet<Resource>(16);
    // 遍歷根路徑下的所有資源與子模式進行匹配,如果匹配成功,則符合路徑模式的資源加入result結果集中
    for (Resource rootDirResource : rootDirResources) {
        rootDirResource = resolveRootDirResource(rootDirResource);
        // 獲取資源的URL
        URL rootDirUrl = rootDirResource.getURL();
        if (equinoxResolveMethod != null) {
            if (rootDirUrl.getProtocol().startsWith("bundle")) {
                rootDirUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
                rootDirResource = new UrlResource(rootDirUrl);
            }
        }
        // 如果URL是vfs協議...,這裏暫時不看這種協議,使用情況較少
        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
        }
        // 如果是jar協議(代表是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[result.size()]);
}

下面逐一介紹findPathMatchingResources的實現細節的方法。

首先determineRootDir決定路徑模式中的通配符前綴部分路徑的解析,作為搜索資源時的根部路徑:

protected String determineRootDir(String location) {
    // 獲取路徑模式中的協議分割符":"位置
    int prefixEnd = location.indexOf(‘:‘) + 1;
    // 路徑總長度
    int rootDirEnd = location.length();
    // 截取協議分割符後的路徑部分,並判斷是否有通配符。(需要去掉協議部分,因為存在classpath*:前綴,會影響ant模式匹配)
    // "/"代表一層目錄,while循環逐層目錄進行截取,判斷是否匹配通配符,然後截取,直到沒有通配符的前綴路徑匹配到結束
    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);
}

通過以上方式,可以獲取Ant風格路徑模式中的通配符之前的目錄部分路徑。然後以此搜索全部資源,再次遞歸調用getResources方法。會進入1-2分支,因為路徑仍然以classpath*開頭,且不包含模式。關於1-2分支後續再介紹,這裏接著分析resolveRootDirResource實現:

// 主要用於子類覆蓋實現
protected Resource resolveRootDirResource(Resource original) throws IOException {
    return original;
}

上述方法主要用於子類擴展實現,默認沒有任何邏輯實現。findPathMatchingResources的後續邏輯主要實現已經查找的資源和子路徑模式的匹配,分為三類匹配:

  • VFS協議前提下,匹配資源與子路徑模式
  • Jar協議前提下,匹配資源與子路徑模式
  • 文件系統中匹配資源與子路徑模式

關於VFS協議使用情況較少,這裏不做分析。主要分析JAR和文件系統下的實現。首先介紹Jar協議中的匹配實現。

if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
    result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
// 如果是jar協議或者是jar資源
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
    // jar資源匹配
    result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
    // 從文件系統中匹配
    result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}

在spring中以下協議的資源都認為是Jar資源

/** URL protocol for an entry from a jar file: "jar" */
public static final String URL_PROTOCOL_JAR = "jar";
/** URL protocol for an entry from a war file: "war" */
public static final String URL_PROTOCOL_WAR = "war";
/** URL protocol for an entry from a zip file: "zip" */
public static final String URL_PROTOCOL_ZIP = "zip";
/** URL protocol for an entry from a WebSphere jar file: "wsjar" */
public static final String URL_PROTOCOL_WSJAR = "wsjar";
/** URL protocol for an entry from a JBoss jar file: "vfszip" */
public static final String URL_PROTOCOL_VFSZIP = "vfszip";

public static boolean isJarURL(URL url) {
    // 獲取URL協議
    String protocol = url.getProtocol();
    // 如果匹配以下協議,都認為是Jar URL
    return (URL_PROTOCOL_JAR.equals(protocol) || URL_PROTOCOL_WAR.equals(protocol) ||
            URL_PROTOCOL_ZIP.equals(protocol) || URL_PROTOCOL_VFSZIP.equals(protocol) ||
            URL_PROTOCOL_WSJAR.equals(protocol));
}

再看spring中如果匹配Jar資源與子模式doFindPathMatchingJarResources的實現

@SuppressWarnings("deprecation")
protected Set<Resource> doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern)
        throws IOException {
    // doFindPathMatchingJarResources即將被移除,默認也沒做任何邏輯,空實現
    // Check deprecated variant for potential overriding first...
    Set<Resource> result = doFindPathMatchingJarResources(rootDirResource, subPattern);
    if (result != null) {
        return result;
    }
    // 根據URL獲取URLConnection(前文已經介紹URLConnection)
    URLConnection con = rootDirURL.openConnection();
    JarFile jarFile;
    String jarFileUrl;
    String rootEntryPath;
    boolean closeJarFile;
    // 如果是JarURLConnection的實例,大多數都是該場景
    if (con instanceof JarURLConnection) {
        // Should usually be the case for traditional JAR files.
        // 轉換類型
        JarURLConnection jarCon = (JarURLConnection) con;
        ResourceUtils.useCachesIfNecessary(jarCon);
        // 根據JarURLConnection獲取JarFile對象
        jarFile = jarCon.getJarFile();
        jarFileUrl = jarCon.getJarFileURL().toExternalForm();
        JarEntry jarEntry = jarCon.getJarEntry();
        rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
        closeJarFile = !jarCon.getUseCaches();
    }
    else {
        // No JarURLConnection -> need to resort to URL file parsing.
        // We‘ll assume URLs of the format "jar:path!/entry", with the protocol
        // being arbitrary as long as following the entry format.
        // We‘ll also handle paths with and without leading "file:" prefix.
        String urlFile = rootDirURL.getFile();
        try {
            int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR);
            if (separatorIndex == -1) {
                separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
            }
            if (separatorIndex != -1) {
                jarFileUrl = urlFile.substring(0, separatorIndex);
                rootEntryPath = urlFile.substring(separatorIndex + 2);  // both separators are 2 chars
                jarFile = getJarFile(jarFileUrl);
            }
            else {
                jarFile = new JarFile(urlFile);
                jarFileUrl = urlFile;
                rootEntryPath = "";
            }
            closeJarFile = true;
        }
        catch (ZipException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Skipping invalid jar classpath entry [" + urlFile + "]");
            }
            return Collections.emptySet();
        }
    }
    try {
        if (logger.isDebugEnabled()) {
            logger.debug("Looking for matching resources in jar file [" + jarFileUrl + "]");
        }
        if (!"".equals(rootEntryPath) && !rootEntryPath.endsWith("/")) {
            // Root entry path must end with slash to allow for proper matching.
            // The Sun JRE does not return a slash here, but BEA JRockit does.
            rootEntryPath = rootEntryPath + "/";
        }
        result = new LinkedHashSet<Resource>(8);
        // 獲取Jar文件中的條目,通過JarFile獲取JarEntry對象,然後遍歷每個條目即JarEntry
        for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
            JarEntry entry = entries.nextElement();
            // 獲取條目的路徑
            String entryPath = entry.getName();
            if (entryPath.startsWith(rootEntryPath)) {
                String relativePath = entryPath.substring(rootEntryPath.length());
                // 對條目路徑的相對部分和路徑中的子模式進行模式匹配,這裏使用AntPathMatcher進行匹配實現
                if (getPathMatcher().match(subPattern, relativePath)) {
                    // 如果匹配成功,則表示是路徑模式匹配的資源,加入結果集
                    result.add(rootDirResource.createRelative(relativePath));
                }
            }
        }
        return result;
    }
    finally {
        if (closeJarFile) {
            jarFile.close();
        }
    }
}

通過Jar文件的URL獲取JarURLConnection,再獲取JarFile,對JarFile中的JarEntry進行遍歷,匹配路徑子模式,如果匹配成功,則是符合路徑模式的資源。其中涉及的很多其他的細節,這裏不做詳述。

接下來再看文件系統資源的匹配實現doFindPathMatchingFileResources

protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
        throws IOException {
    File rootDir;
    try {
        // 獲取資源的文件系統絕對路徑
        rootDir = rootDirResource.getFile().getAbsoluteFile();
    }
    catch (IOException ex) {
        if (logger.isWarnEnabled()) {
            logger.warn("Cannot search for matching files underneath " + rootDirResource +
                    " because it does not correspond to a directory in the file system", ex);
        }
        return Collections.emptySet();
    }
    // 根據絕對路徑和子模式進行匹配
    return doFindMatchingFileSystemResources(rootDir, subPattern);
}

doFindMatchingFileSystemResources中實現文件系統資源匹配:

protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
    if (logger.isDebugEnabled()) {
        logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
    }
    // retrieveMatchingFiles實現匹配並獲取匹配資源的File對象
    Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
    Set<Resource> result = new LinkedHashSet<Resource>(matchingFiles.size());
    // 根據匹配資源的File對象創建FileSystemResource
    for (File file : matchingFiles) {
        result.add(new FileSystemResource(file));
    }
    return result;
}

這裏核心的方法就是retrieveMatchingFiles,它實現了如果根據文件系統的絕對路徑查找匹配子模式的文件資源:

protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
    // 如果文件目錄不存在,則返回空資源,表示沒有搜索到匹配子模式的文件資源
    if (!rootDir.exists()) {
        // Silently skip non-existing directories.
        if (logger.isDebugEnabled()) {
            logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
        }
        return Collections.emptySet();
    }
    // 如果不是目錄,則返回空資源
    if (!rootDir.isDirectory()) {
        // Complain louder if it exists but is no directory.
        if (logger.isWarnEnabled()) {
            logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
        }
        return Collections.emptySet();
    }
    // 如果沒有目錄的讀權限,則返回空資源
    if (!rootDir.canRead()) {
        if (logger.isWarnEnabled()) {
            logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() +
                    "] because the application is not allowed to read the directory");
        }
        return Collections.emptySet();
    }
    // 將不同文件系統的目錄分隔符統一轉換成"/"分隔符
    String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
    if (!pattern.startsWith("/")) {
        fullPattern += "/";
    }
    // 轉換子模式中的分割符為"/"
    fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
    // 很好的編程習慣
    Set<File> result = new LinkedHashSet<File>(8);
    // 根據路徑和模式匹配
    doRetrieveMatchingFiles(fullPattern, rootDir, result);
    return result;
}

retrieveMatchingFiles主要做目錄的校驗和路徑分隔符的轉換。實現匹配的核心部分在doRetrieveMatchingFiles:

protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
    if (logger.isDebugEnabled()) {
        logger.debug("Searching directory [" + dir.getAbsolutePath() +
                "] for files matching pattern [" + fullPattern + "]");
    }
    // 獲取目錄中的所有文件
    File[] dirContents = dir.listFiles();
    // 如果目錄中沒有文件,則返回空,表示沒有搜索到任何匹配的資源
    if (dirContents == null) {
        if (logger.isWarnEnabled()) {
            logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
        }
        return;
    }
    Arrays.sort(dirContents);
    // 遍歷所有文件
    for (File content : dirContents) {
        // 轉換分隔符
        String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
        // 如果是目錄,且路徑匹配完整模式
        if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
            // 如果無讀權限
            if (!content.canRead()) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
                            "] because the application is not allowed to read the directory");
                }
            }
            else {
                // 如果有讀權限,則遞歸調用
                doRetrieveMatchingFiles(fullPattern, content, result);
            }
        }
        // 如果是文件,則判斷文件路徑和模式,匹配成功則加入結果集
        if (getPathMatcher().match(fullPattern, currPath)) {
            result.add(content);
        }
    }
}

以上便是spring中按照模式從文件系統搜索匹配文件資源的過程。首先根據模式中的統配符前綴路徑獲取文件目錄,然後獲取文件目錄中的所有文件,遍歷所有文件。如果是子目錄,則判斷子目錄路徑是否匹配模式,匹配則遞歸調用再匹配;如果是子文件,則判斷子文件路徑是否匹配模式,匹配則加入結果集。

根據類路徑模式搜索資源主要是以上三種方式。接下來再分析完整類路徑搜索匹配資源的詳細過程。

2.1-2分支(完整類路徑資源)

完整類路徑搜索匹配資源主要有1-2分支負責,由findAllClassPathResources實現:

完整類路徑資源搜索主要委托Java ClassLoader加載資源。關於ClassLoader加載資源的詳情這裏不詳述。

// 截取路徑,去掉路徑的中類路徑前綴"classpath*:"
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
protected Resource[] findAllClassPathResources(String location) throws IOException {
    String path = location;
    // 截取路徑的前導"/",表示該資源路徑相對於類類路徑
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    // 在所有類路徑(jar、工程)尋找資源
    Set<Resource> result = doFindAllClassPathResources(path);
    if (logger.isDebugEnabled()) {
        logger.debug("Resolved classpath location [" + location + "] to resources " + result);
    }
    return result.toArray(new Resource[result.size()]);
}

在所有類路徑中尋找資源的核心實現由doFindAllClassPathResources完成:

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    // nice的寫法
    Set<Resource> result = new LinkedHashSet<Resource>(16);
    // 獲取類類加載器,這裏由AbstractApplicationContext中初始化PathMatchingResourcePatternResolver指定ResourceLoader
    // ResourceLoader是AbstractApplicationContext繼承DefaultResourceLoader,類加載由DefaultResourceLoader的構造方法中初始化
    // DefaultResourceLoader中使用ClassUtils.getDefaultClassLoader()獲取類加載器
    ClassLoader cl = getClassLoader();
    // 使用類加載器加載指定路徑的所有資源
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    while (resourceUrls.hasMoreElements()) {
        URL url = resourceUrls.nextElement();
        // 將url轉換為UrlResource,放入結果集中
        result.add(convertClassLoaderURL(url));
    }
    // 如果路徑是""空串,則使用ClassLoader將無法加載到任何資源
    // 這是需要使用URLClassLoader獲取所有類和資源搜索路徑,然後逐一遍歷查找
    if ("".equals(path)) {
        // The above result is likely to be incomplete, i.e. only containing file system references.
        // We need to have pointers to each of the jar files on the classpath as well...
        addAllClassLoaderJarRoots(cl, result);
    }
    return result;
}

這裏關鍵點在於使用ClassLoader類加載提供的加載類路徑資源的能力,當時對於提供的""空路徑(如:classpath*:*.xml),類加載器將無法加載。需要使用URLClassLoader提供的獲取搜索所有類和資源路徑的接口。

再介紹addAllClassLoaderJarRoots實現細節:

protected void addAllClassLoaderJarRoots(ClassLoader classLoader, Set<Resource> result) {
    // 判斷當前classLoader是否為URLClassLoader實例
    if (classLoader instanceof URLClassLoader) {
        try {
            // 獲取所有資源和類的搜索路徑ucp
            for (URL url : ((URLClassLoader) classLoader).getURLs()) {
                try {
                    // 根據jar url規則組裝url並創建UrlResource對象
                    UrlResource jarResource = new UrlResource(
                            ResourceUtils.JAR_URL_PREFIX + url + ResourceUtils.JAR_URL_SEPARATOR);
                    // 判斷資源是否存在,排除非jar資源的類路徑
                    if (jarResource.exists()) {
                        result.add(jarResource);
                    }
                }
                catch (MalformedURLException ex) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Cannot search for matching files underneath [" + url +
                                "] because it cannot be converted to a valid ‘jar:‘ URL: " + ex.getMessage());
                    }
                }
            }
        }
        catch (Exception ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Cannot introspect jar files since ClassLoader [" + classLoader +
                        "] does not support ‘getURLs()‘: " + ex);
            }
        }
    }
    // 如果類加載器為空使用系統類加載器,需要處理java.class.path引用的jar
    if (classLoader == ClassLoader.getSystemClassLoader()) {
        // "java.class.path" manifest evaluation...
        addClassPathManifestEntries(result);
    }
    // 父類加載器的資源和所有類
    if (classLoader != null) {
        try {
            // Hierarchy traversal...
            addAllClassLoaderJarRoots(classLoader.getParent(), result);
        }
        catch (Exception ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Cannot introspect jar files in parent ClassLoader since [" + classLoader +
                        "] does not support ‘getParent()‘: " + ex);
            }
        }
    }
}

對於完整類路徑資源的加載主要分為兩種模式:

  • 委托ClassLoader的getResources接口加載資源
  • 當資源path為空串時,使用加載器的ucp和java.class.path指定的資源和類
3.2-1分支(文件模式-文件系統"file://"或者類路徑"classpath:"模式)
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
    // a file pattern
    return findPathMatchingResources(locationPattern);
}

主要由findPathMatchingResources實現,前文中已經詳述實現細節,可以對照參考。文件模式大致流程和類路徑模式處理流程類似。找到文件模式的通配符前的根目錄,然後再次遞歸調用加載根目錄下的所有資源,再遍歷依次匹配子模式,最終檢索符合的資源。在這裏遞歸調用加載根目錄的所有資源時,會進入2-2分支

// 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);
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
    // a file pattern
    return findPathMatchingResources(locationPattern);
}
else {
    // 遞歸調用時,路徑模式通配符前的根目錄非classpath*:開頭並且非ant風格
    // a single resource with the given name
    return new Resource[] {getResourceLoader().getResource(locationPattern)};
}

對於單個完整文件路徑資源的加載在spring中有ResourceLoader負責,PathMatchingResourcePatternResolver中組合ResourceLoader的默認實現DefaultResourceLoader。這裏對於單個完整文件路徑的資源(在文件系統或者類路徑上)加載委托DefaultResourceLoader加載。關於其實現細節,前文中也已經介紹。

4.2-2分支(單個文件路徑-類路徑或者文件系統)

上節中也已經介紹,對於單個文件的加載PathMatchingResourcePatternResolver委托DefaultResourceLoader實現。ResourceLoader

Tips
Spring中大量使用接口隔離,單一職責。然後通過組合方式實現超強的處理能力和極易擴展的能力。ResourceLoader和ResourcePatternResolver接口隔離,單一職責,PathMatchingResourcePatternResolver組合DefaultResourceLoader。

何時何地觸發加載解析

在介紹Spring如何加載Resource資源的細節後,需要全面了解Spring如何觸發,什麽時機觸發資源的加載。

技術分享圖片

上圖中描繪了Spring加載資源的上下文過程:

  1. 將資源抽象(路徑)給上下文
  2. 上下文委托BeanDefinitonReader加載
  3. BeanDefinitonReader委托ResourceLoader資源加載資源
  4. ResourceLoader最終生產資源抽象的對象Resource

BeanDefinitonReader和ResourceLoader的完整UML圖關系:

技術分享圖片

當使用XML定義Bean的元數據時,Spring將使用XmlBeanDefinitionReader。關於如何實現的細節下節BeanDefinition中介紹詳細。

總結

1.資源
前綴 例子 描述
classpath: classpath:com/myapp/config.xml 從類路徑加載,解析為ClassPathResource
file: file:///data/config.xml 從文件系統加載,解析文FileSystemResrouce
http: http://myserver/logo.png 從http URL加載,解析為UrlResource
(none) /data/config.xml 依賴上下文類型

Spring中對於不同的路徑解析為不同類型的資源。

2.解析
  • 單個資源加載。資源可以以文件系統或者類路徑方式表現,由ResourceLoader負責解析加載
  • 路徑模式,主要是Ant-Style。支持classpath:*和模式匹配方式解析加載資源
參考

springframework docs

Spring源碼系列 — Resource抽象