Spring 源碼第一篇開整!配置文件是怎麼加載的?

上週把話撂出來,看起來小夥伴們都挺期待的,其實鬆哥也火燒眉毛想要開啓一個全新的系列。java

可是目前的 Spring Security 系列還在連載中,還沒寫完。連載這事,一氣呵成,再而衰三而竭,必定要一次搞定,Spring Security 若是此次放下來,之後就很難再拾起來了。spring

因此目前的更新仍是 Spring Security 爲主,同時 Spring 源碼解讀每週至少更新一篇,等 Spring Security 系列更新完畢後,就開足馬力更新 Spring 源碼。其實 Spring Security 中也有不少和 Spring 相通的地方,Spring Security 你們文章認真看,鬆哥不會讓你們失望的!app

1.從何提及

Spring 要從何提及呢?這個問題我考慮了很長時間。框架

由於 Spring 源碼太繁雜了,必定要選擇一個合適的切入點,不然一上來就把各位小夥伴整懵了,那剩下的文章估計就不想看了。ide

想了好久以後,我決定就先從配置文件加載講起,在逐步展開,配置文件加載也是咱們在使用 Spring 時遇到的第一個問題,今天就先來講說這個話題。ui

2.簡單的案例

先來一個簡單的案例,你們感覺一下,而後咱們順着案例講起。this

首先咱們建立一個普通的 Maven 項目,引入 spring-beans 依賴:編碼

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

而後咱們建立一個實體類,再添加一個簡單的配置文件:url

public class User {
    private String username;
    private String address;
    //省略 getter/setter
}

resources 目錄下建立配置文件:spa

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="org.javaboy.loadxml.User" id="user"/>
</beans>

而後去加載這個配置文件:

public static void main(String[] args) {
    XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
    User user = factory.getBean(User.class);
    System.out.println("user = " + user);
}

這裏爲了展現數據的讀取過程,我就先用這個已通過期的 XmlBeanFactory 來加載,這並不影響咱們閱讀源碼。

上面這個是一個很是簡單的 Spring 入門案例,相信不少小夥伴在第一次接觸 Spring 的時候,寫出來的可能都是這個 Demo。

在上面這段代碼執行過程當中,首先要作的事情就是先把 XML 配置文件加載到內存中,再去解析它,再去。。。。。

一步一步來吧,先來看 XML 文件如何被加入到內存中去。

3.文件讀取

文件讀取在 Spring 中很常見,也算是一個比較基本的功能,並且 Spring 提供的文件加載方式,不只僅在 Spring 框架中可使用,咱們在項目中有其餘文件加載需求也可使用。

首先,Spring 中使用 Resource 接口來封裝底層資源,Resource 接口自己實現自 InputStreamSource 接口:

咱們來看下這兩個接口的定義:

public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
}
public interface Resource extends InputStreamSource {
    boolean exists();
    default boolean isReadable() {
        return exists();
    }
    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;
    @Nullable
    String getFilename();
    String getDescription();

}

代碼倒不難,我來稍微解釋下:

  1. InputStreamSource 類只提供了一個 getInputStream 方法,該方法返回一個 InputStream,也就是說,InputStreamSource 會將傳入的 File 等資源,封裝成一個 InputStream 再從新返回。
  2. Resource 接口實現了 InputStreamSource 接口,而且封裝了 Spring 內部可能會用到的底層資源,如 File、URL 以及 classpath 等。
  3. exists 方法用來判斷資源是否存在。
  4. isReadable 方法用來判斷資源是否可讀。
  5. isOpen 方法用來判斷資源是否打開。
  6. isFile 方法用來判斷資源是不是一個文件。
  7. getURL/getURI/getFile/readableChannel 分別表示獲取資源對應的 URL/URI/File 以及將資源轉爲 ReadableByteChannel 通道。
  8. contentLength 表示獲取資源的大小。
  9. lastModified 表示獲取資源的最後修改時間。
  10. createRelative 表示根據當前資源建立一個相對資源。
  11. getFilename 表示獲取文件名。
  12. getDescription 表示在資源出錯時,詳細打印出出錯的文件。

當咱們加載不一樣資源時,對應了 Resource 的不一樣實現類,來看下 Resource 的繼承關係:

能夠看到,針對不一樣類型的數據源,都有各自的實現,咱們這裏來重點看下 ClassPathResource 的實現方式。

ClassPathResource 源碼比較長,我這裏挑一些關鍵部分來和你們分享:

public class ClassPathResource extends AbstractFileResolvingResource {

    private final String path;

    @Nullable
    private ClassLoader classLoader;

    @Nullable
    private Class<?> clazz;

    public ClassPathResource(String path) {
        this(path, (ClassLoader) null);
    }
    public ClassPathResource(String path, @Nullable 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());
    }
    public ClassPathResource(String path, @Nullable Class<?> clazz) {
        Assert.notNull(path, "Path must not be null");
        this.path = StringUtils.cleanPath(path);
        this.clazz = clazz;
    }
    public final String getPath() {
        return this.path;
    }
    @Nullable
    public final ClassLoader getClassLoader() {
        return (this.clazz != null ? this.clazz.getClassLoader() : this.classLoader);
    }
    @Override
    public boolean exists() {
        return (resolveURL() != null);
    }
    @Nullable
    protected URL resolveURL() {
        if (this.clazz != null) {
            return this.clazz.getResource(this.path);
        }
        else if (this.classLoader != null) {
            return this.classLoader.getResource(this.path);
        }
        else {
            return ClassLoader.getSystemResource(this.path);
        }
    }
    @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;
    }
    @Override
    public URL getURL() throws IOException {
        URL url = resolveURL();
        if (url == null) {
            throw new FileNotFoundException(getDescription() + " cannot be resolved to URL because it does not exist");
        }
        return url;
    }
    @Override
    public Resource createRelative(String relativePath) {
        String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
        return (this.clazz != null ? new ClassPathResource(pathToUse, this.clazz) :
                new ClassPathResource(pathToUse, this.classLoader));
    }
    @Override
    @Nullable
    public String getFilename() {
        return StringUtils.getFilename(this.path);
    }
    @Override
    public String getDescription() {
        StringBuilder builder = new StringBuilder("class path resource [");
        String pathToUse = this.path;
        if (this.clazz != null && !pathToUse.startsWith("/")) {
            builder.append(ClassUtils.classPackageAsResourcePath(this.clazz));
            builder.append('/');
        }
        if (pathToUse.startsWith("/")) {
            pathToUse = pathToUse.substring(1);
        }
        builder.append(pathToUse);
        builder.append(']');
        return builder.toString();
    }
}
  1. 首先,ClassPathResource 的構造方法有四個,一個已通過期的方法我這裏沒有列出來。另外三個,咱們通常調用一個參數的便可,也就是傳入文件路徑便可,它內部會調用另一個重載的方法,給 classloader 賦上值(由於在後面要經過 classloader 去讀取文件)。
  2. 在 ClassPathResource 初始化的過程當中,會先調用 StringUtils.cleanPath 方法對傳入的路徑進行清理,所謂的路徑清理,就是處理路徑中的相對地址、Windows 系統下的 \\ 變爲 / 等。
  3. getPath 方法用來返回文件路徑,這是一個相對路徑,不包含 classpath。
  4. resolveURL 方法表示返回資源的 URL,返回的時候優先用 Class.getResource 加載,而後纔會用 ClassLoader.getResource 加載,關於 Class.getResource 和 ClassLoader.getResource 的區別,又能寫一篇文章出來,我這裏就大概說下,Class.getResource 最終仍是會調用 ClassLoader.getResource,只不過 Class.getResource 會先對路徑進行處理。
  5. getInputStream 讀取資源,並返回 InputStream 對象。
  6. createRelative 方法是根據當前的資源,再建立一個相對資源。

這是 ClassPathResource,另一個你們可能會接觸到的 FileSystemResource ,小夥伴們能夠自行查看其源碼,比 ClassPathResource 簡單。

若是不是使用 Spring,咱們僅僅想本身加載 resources 目錄下的資源,也能夠採用這種方式:

ClassPathResource resource = new ClassPathResource("beans.xml");
InputStream inputStream = resource.getInputStream();

拿到 IO 流以後自行解析便可。

在 Spring 框架,構造出 Resource 對象以後,接下來還會把 Resource 對象轉爲 EncodedResource,這裏會對資源進行編碼處理,編碼主要體如今 getReader 方法上,在獲取 Reader 對象時,若是有編碼,則給出編碼格式:

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

全部這一切搞定以後,接下來就是經過 XmlBeanDefinitionReader 去加載 Resource 了。

4.小結

好啦,今天主要和小夥伴們分享一下 Spring 中的資源加載問題,這是容器啓動的起點,下篇文章咱們來看 XML 文件的解析。

若是小夥伴們以爲有收穫,記得點個在看鼓勵下鬆哥哦~

相關文章
相關標籤/搜索