(珍藏版)Spring 源碼解析:配置文件的加載

一、簡單案例引入

首先我們創建一個普通的 Maven 項目,引入 spring-beans 依賴:

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

然後我們創建一個實體類,再添加一個簡單的配置文件:

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

resources 目錄下創建配置文件:

<?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 配置文件加載到內存中,再去解析它,再去。。。。。

二、文件讀取

文件讀取在 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();

}

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

  • InputStreamSource 類只提供了一個 getInputStream 方法,該方法返回一個 InputStream,也就是說,InputStreamSource 會將傳入的 File 等資源,封裝成一個 InputStream 再重新返回。
  • Resource 接口實現了 InputStreamSource 接口,並且封裝了 Spring 內部可能會用到的底層資源,如 File、URL 以及 classpath 等。
  • exists 方法用來判斷資源是否存在。
  • isReadable 方法用來判斷資源是否可讀。
  • isOpen 方法用來判斷資源是否打開。
  • isFile 方法用來判斷資源是否是一個文件。
  • getURL/getURI/getFile/readableChannel 分別表示獲取資源對應的 URL/URI/File 以及將資源轉爲 ReadableByteChannel 通道。
  • contentLength 表示獲取資源的大小。
  • lastModified 表示獲取資源的最後修改時間。
  • createRelative 表示根據當前資源創建一個相對資源。
  • getFilename 表示獲取文件名。
  • 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();
    }
}
  • 首先,ClassPathResource 的構造方法有四個,一個已經過期的方法我這裏沒有列出來。另外三個,我們一般調用一個參數的即可,也就是傳入文件路徑即可,它內部會調用另外一個重載的方法,給 classloader 賦上值(因爲在後面要通過 classloader 去讀取文件)。
  • 在 ClassPathResource 初始化的過程中,會先調用 StringUtils.cleanPath 方法對傳入的路徑進行清理,所謂的路徑清理,就是處理路徑中的相對地址、Windows 系統下的 \ 變爲 / 等。
  • getPath 方法用來返回文件路徑,這是一個相對路徑,不包含 classpath。
  • resolveURL 方法表示返回資源的 URL,返回的時候優先用 Class.getResource 加載,然後纔會用 ClassLoader.getResource 加載,關於 Class.getResource 和 ClassLoader.getResource 的區別,又能寫一篇文章出來,我這裏就大概說下
  • Class.getResource 最終還是會調用 ClassLoader.getResource,只不過 Class.getResource 會先對路徑進行處理。
  • getInputStream 讀取資源,並返回 InputStream 對象。
  • 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 了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章