【修煉內功】[spring-framework][1]Resource

本文已收錄 【修煉內功】躍遷之路

spring-framework.jpg

林中小舍.png

閱讀源碼是一件極其枯燥無比的事情,對於使用頻率較高的組件,若是能作到知其然且知其因此然,這對平常工做中不管是問題排查、代碼優化、功能擴展等都是利大於弊的,如同老司機開車(對,就是開車),會讓你有一種參與感,而不只僅把它當成一種工具,若能習之精髓、學以至用,那便再好不過!html

從工做之初便開始接觸Spring框架,時至今日也沒有認真地正視過它的實現細節,今日開拔,但願可以堅持下來~java

對於Spring如此「龐大」(至少與我而言)的框架,不想一上來就將level提的很高,以上帝的視角將整個Spring框架的架構圖或者類圖之類拋出來,對於並不特別瞭解的人來講,除了膜拜Spring的「宏偉」以外別無他法,依然不清楚應該如何下手git

這裏,但願可以按部就班,將Spring的幾個核心組件各個擊破,再將各組件串聯起來,以點至面github

Spring系列文章web

  • 默認您對Spring框架有必定的瞭解及使用經驗
  • 既然是源碼分析便不可避免地會貼一些源碼,會盡可能以精簡代碼、僞代碼、額外註釋等方式呈現,以減小源碼所佔的篇幅

言歸正傳,本篇就Spring的資源Resource聊起spring

Resource

Resource(資源)是進入Spring生態的第一道門,不敢說它是Spring的基石,但絕對是Spring的核心組件之一segmentfault

Resource主要負責資源的(讀寫)操做,最爲常見地出如今系統各初始化階段,如網絡

  • 指定配置文件,手動建立ApplicationContext
new ClassPathXmlApplicationContext("classpath:spring/application.xml");
  • 在web.xml中指定配置文件,用於Spring初始化時加載
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/application.xml</param-value>
</context-param>
  • 指定package,在掃描過程當中查找指定package下class文件
@Configuration
@ComponentScan("com.manerfan")
public class AppConfiguration {}
<context:component-scan base-package="com.manerfan"></context:component-scan>
  • (Spring Boot中)指定mybatis配置,用於mybatis初始化時加載
mybatis.config-location=classpath:/mybatis/mybatis-config.xml
  • 依賴注入時加載指定資源文件
@Component
public class SomeComponent {
    @Value("classpath:in18/zh-cn.properties")
    private Resource in18ZhCn;
}
<bean id="someComponent" class="...SomeComponent">
    <property name="in18ZhCn" value="classpath:in18/zh-cn.properties"/>
</bean>
  • Spring Boot加載啓動配置文件application[-env].properties application[-env].yml,加載META-INF配置文件等

Java中的URL經過不一樣的前綴(協議)已經實現了一套資源的讀取,如磁盤文件file:///var/log/system.log、網絡文件https://some.host/some.file.txt甚至jar中的classjar:file:///spring-core.jar!/org/springframework/core/io/Resource.class,然而Spring並無採用URL的方案,其官方文檔給出了必定的解釋mybatis

https://docs.spring.io/spring...

Java’s standard java.net.URL class and standard handlers for various URL prefixes, unfortunately, are not quite adequate enough for all access to low-level resources. For example, there is no standardized URL implementation that may be used to access a resource that needs to be obtained from the classpath or relative to a ServletContext. While it is possible to register new handlers for specialized URL prefixes (similar to existing handlers for prefixes such as http:), this is generally quite complicated, and the URL interface still lacks some desirable functionality, such as a method to check for the existence of the resource being pointed to.架構

其一,URL擴展複雜;其二,URL功能有限

Spring將不一樣類型的資源統一抽象成了Resource,這有點相似Linux系統的「一切皆文件」(磁盤文件、目錄、硬件設備、套接字、網絡等),資源的抽象屏蔽了不一樣類型資源的差別性,統一了操做接口

⇪Resource的定義很是簡潔明瞭,方法的命名已經足夠清晰,再也不統一解釋

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

Resource繼承自更爲抽象的⇪InputStreamSource

public interface InputStreamSource {
    String CLASSPATH_URL_PREFIX = "classpath:";
    InputStream getInputStream() throws IOException;
}

其只有一個方法getInputStream,用於獲取資源的InputStream

對於Resouce的具體實現可參考下圖(莫被錯綜複雜的類關係擾亂了思路),一層層剝離解析

Core.Resource.png

WritableResource

⇪WritableResource派生自Resource,其在Resource的基礎上增長了'寫'相關的能力

public interface WritableResource extends Resource {
    boolean isWritable();
    OutputStream getOutputStream() throws IOException;
    WritableByteChannel writableChannel() throws IOException;
}

這裏重點關注Resource'讀'能力的實現

AbstractResource

⇪AbstractResource實現了大部分Resource中公共的、無底層差別的邏輯,實現較爲簡單,再也不詳述

AbstractResource的具體實現類則是封裝了不一樣類型的資源類庫,使用具體的類庫函數實現Resource定義的一系列接口

⇪FileSystemResource封裝了java.io.File(或java.io.Path)的能力實現了Resource的一些細節

public class FileSystemResource extends AbstractResource implements WritableResource {
    @Override
    public InputStream getInputStream() throws IOException {
        // ...
        // 將File/Path封裝爲InputStream
        return Files.newInputStream(this.filePath);
        // ...
    }
    
    @Override
    public OutputStream getOutputStream() throws IOException {
        // 將File/Path封裝爲OutputStream
        return Files.newOutputStream(this.filePath);
    }
    
    @Override
    public URL getURL() throws IOException {
        return (this.file != null ? this.file.toURI().toURL() : this.filePath.toUri().toURL());
    }

    @Override
    public URI getURI() throws IOException {
        return (this.file != null ? this.file.toURI() : this.filePath.toUri());
    }
    
    // ...
}

同理,⇪ByteArrayResource封裝了ByteArray的能力,⇪InputStreamResource封裝了InputSream的能力,等等,再也不一一介紹

AbstractFileResolvingResource

⇪AbstractFileResolvingResource則把中心放在java.net.URL上,其使用URL的能力重寫了其父類AbstractResource的大部分實現

AbstractFileResolvingResource的實現類只有兩個,⇪UrlResource⇪ClassPathResource

UrlResource

UrlResource一樣簡單地封裝了URL的能力來實現Resource中定義的接口

public class UrlResource extends AbstractFileResolvingResource {
    @Override
    public InputStream getInputStream() throws IOException {
        URLConnection con = this.url.openConnection();
        ResourceUtils.useCachesIfNecessary(con);
        try {
            return con.getInputStream();
        }
        catch (IOException ex) {
            // Close the HTTP connection (if applicable).
            if (con instanceof HttpURLConnection) {
                ((HttpURLConnection) con).disconnect();
            }
            throw ex;
        }
    }
    
    // ...
}
ClassPathResource

ClassPathResource則是藉助ClassLoader的能力來實現Resource中定義的接口

public class ClassPathResource extends AbstractFileResolvingResource {
    @Override
    public InputStream getInputStream() throws IOException {
        InputStream is;
        if (this.clazz != null) {
            is = this.clazz.getResourceAsStream(this.path);
        }
        else if (this.classLoader != null) {
            is = this.classLoader.getResourceAsStream(this.path);
        }
        else {
            is = ClassLoader.getSystemResourceAsStream(this.path);
        }
        if (is *** null) {
            throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
        }
        return is;
    }
}

EncodedResource

細心的可能會發現,這裏還有一個⇪EncodedResource,其在Resource的基礎上加入了編碼信息,並提供了額外的getReader接口

public class EncodedResource implements InputStreamSource {
    public Reader getReader() throws IOException {
        if (this.charset != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.charset);
        }
        else if (this.encoding != null) {
            return new InputStreamReader(this.resource.getInputStream(), this.encoding);
        }
        else {
            return new InputStreamReader(this.resource.getInputStream());
        }
    }
}

這對於獲取編碼格式有要求的資源來說十分受用

@Component
public class MyComponent {
    private final Properties properties;
    public MyComponent(@Value("classpath:/config/my-config.properties") Resource resource) {
        // Properties讀取配置默認編碼爲ISO-8859-1
        this.properties = PropertiesLoaderUtils.loadProperties(new EncodedResource(resource, "utf-8"));
    }
    
    // ...
}

小結

使用上,針對不一樣的資源類型建立不一樣的Resource便可

new FileSystemResource("/var/log/system.log"); // 文件系統中的文件
new ClassPathResource("/config/my-config.properties"); // classpath中的文件
new UrlResource("http://oss.manerfan.com/config/my-config.properties"); // 網絡上的文件

ResourceLoader

針對不一樣的資源類型建立不一樣的Resource」,如上例中的硬編碼並不符合開閉原則,對於開發者來講其實並不那麼友好,還好Spring提供了⇪ResourceLoader (接下來,你會發現Spring中提供了各類各樣的LoaderResolverAware等等)

public interface ResourceLoader {
    Resource getResource(String location);
}

ResourceLoader中定義了getResource方法用於建立合適類型的Resource,至於應該建立哪一種類型以及如何建立,則交由ResourceLoader處理

ResourceLoader的實現類主要有兩種,其一爲⇪DefaultResourceLoader,其二爲⇪PathMatchingResourcePatternResolver

Core.ResourceLoader.png

DefaultResourceLoader

⇪DefaultResourceLoaderResourceLoader的默認實現,其實現極爲簡單

public class DefaultResourceLoader implements ResourceLoader {
    @Override
    public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");

        // 1. 若是存在自定義的解析器,優先使用自定義解析器
        
        for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
            Resource resource = protocolResolver.resolve(location, this);
            if (resource != null) {
                return resource;
            }
        }

        // 2. 若是沒有自定義解析器,或者自定義解析器沒法解析,則使用默認實現
        
        if (location.startsWith("/")) {
            // 構造ClassPathResource
            return getResourceByPath(location);
        }
        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            // 取"classpath:"後的內容,構造ClassPathResource
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }
        else {
            try {
                // 嘗試構造URLResource
                URL url = new URL(location);
                return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
            }
            catch (MalformedURLException ex) {
                // 降級到ClassPathResource
                return getResourceByPath(location);
            }
        }
    }
    
    protected Resource getResourceByPath(String path) {
        return new ClassPathContextResource(path, getClassLoader());
    }
}

首先會嘗試使用自定義解析器解析(經過addProtocolResolver方法添加),若是沒有或者解析失敗纔會使用默認實現

默認實現邏輯中,若是是以/classpath:開頭的,會直接構造ClassPathResource,不然會嘗試構造爲UrlResource

瞭解ClassPathResource實現的會注意到,若是在全部的classpath路徑中同時存在多個文件匹配(如,同時在a.jarb.jar中存在/config/my-config.properties文件),則只會返回首個匹配到的(取決於JVM加載順序),這也是有別於PathMatchingResourcePatternResolver的一個地方

PathMatchingResourcePatternResolver

⇪PathMatchingResourcePatternResolver實現自⇪ResourcePatternResolver接口

public interface ResourcePatternResolver extends ResourceLoader {
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
    Resource[] getResources(String locationPattern) throws IOException;
}

ResourcePatternResolverResourceLoader的基礎上,增長了批量獲取的接口getResources

默認狀況下,PathMatchingResourcePatternResolvergetResource實現實際上是使用了DefaultResourceLoader(固然你也能夠本身指定默認的ResourceLoader實現)

public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
    public PathMatchingResourcePatternResolver() {
        this.resourceLoader = new DefaultResourceLoader();
    }
    
    @Override
    public Resource getResource(String location) {
        return getResourceLoader().getResource(location);
    }
}

PathMatchingResourcePatternResolver的一大特色在於PathMatching(默認使用AntPathMatcher,也能夠指定),對於相似classpath:/config/my-*.propertiesclasspath*:/config/my-*.properties等Ant風格的資源進行匹配,其基本思路大體爲

  1. 找到Ant風格資源的父目錄 classpath:/config/classpath*:/config/
  2. 在該目錄下查找匹配my-*.properties的資源(具體實現集中在⇪findPathMatchingResources方法)

classpath:classpath*:的區別主要在於父目錄的查找邏輯

classpath:

父目錄的查找藉助DefaultResourceLoader的能力(歸根結底使用了ClassLoader.getResource),上文也有提到,這裏只會返回首個匹配到的目錄資源

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    // ... 各類 if-else 以後
    // a single resource with the given name
    return new Resource[] {getResourceLoader().getResource(locationPattern)};
}

classpath*:

父目錄的查找則直接使用ClassLoader.getResources,返回全部classpath中的目錄資源

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    // ... 各類 if-else 以後
    // all class path resources with the given name
    return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}

protected Resource[] findAllClassPathResources(String location) throws IOException {
    // ...
    Set<Resource> result = doFindAllClassPathResources(path);
    // ...
}

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    // ...
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    // ...
}

因此,classpath:/config/my-*.properties只會返回首個匹配到的/config/目錄中全部的my-*.properties資源,而classpath*:/config/my-*.properties則會返回全部匹配到的/config/目錄中全部的my-*.properties資源

AbstractApplicationContext

AbstractApplicationContext同時實現了ResourcePatternResolver接口並繼承了DefaultResourceLoader

public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext {
    public AbstractApplicationContext() {
        this.resourcePatternResolver = getResourcePatternResolver();
    }

    protected ResourcePatternResolver getResourcePatternResolver() {
        return new PathMatchingResourcePatternResolver(this);
    }
}

因此,如開篇的幾個例子裏,在ApplicationContext中獲取Resource資源,大多數狀況下使用的都是PathMatchingResourcePatternResolver

小結

  • Spring提供了ResourceLoader 根據不一樣的前綴(協議)生成相對應的Resource(Spring中提供了各類各樣的LoaderResolverAware等等)
  • DefaultResourceLoader只能獲取單個資源,且只能獲取classpath中首次匹配到的資源(ClassLoader.getResource
  • PathMatchingResourcePatternResolver可使用Ant風格匹配並返回多個資源

    • classpath:前綴,只會返回首個匹配到的根目錄中(ClassLoader.getResource)全部的知足給定Ant規則的資源
    • classpath*:前綴,則會返回全部匹配到的根目錄中(ClassLoader.getResources)全部的知足給定Ant規則的資源
  • ApplicationContext默認使用PathMatchingResourcePatternResolver獲取Resource資源

總結

  • ⇪Resource將資源的操做抽象,屏蔽不一樣類型資源差別性,統一操做接口
  • Resource的不一樣實現,均藉助相應的資源類庫能力,來實現Resource中定義的接口
  • ResourceLoader 根據不一樣的前綴(協議)生成相對應的Resource

    • DefaultResourceLoader只能獲取單個資源,且只能獲取classpath中首次匹配到的資源
    • PathMatchingResourcePatternResolver可使用Ant風格匹配並返回多個資源,classpath:classpath*:的區別在於如何獲取根目錄以在其中查找匹配Ant風格的資源
  • ApplicationContext默認使用PathMatchingResourcePatternResolver獲取Resource資源

訂閱號

相關文章
相關標籤/搜索