Spring 原始碼第一篇開整!配置檔案是怎麼載入的?

_江南一點雨發表於2020-06-15

上週把話撂出來,看起來小夥伴們都挺期待的,其實鬆哥也迫不及待想要開啟一個全新的系列。

但是目前的 Spring Security 系列還在連載中,還沒寫完。連載這事,一鼓作氣,再而衰三而竭,一定要一次搞定,Spring Security 如果這次放下來,以後就很難再拾起來了。

所以目前的更新還是 Spring Security 為主,同時 Spring 原始碼解讀每週至少更新一篇,等 Spring Security 系列更新完畢後,就開足馬力更新 Spring 原始碼。其實 Spring Security 中也有很多和 Spring 相通的地方,Spring Security 大家文章認真看,鬆哥不會讓大家失望的!

1.從何說起

Spring 要從何說起呢?這個問題我考慮了很長時間。

因為 Spring 原始碼太繁雜了,一定要選擇一個合適的切入點,否則一上來就把各位小夥伴整懵了,那剩下的文章估計就不想看了。

想了很久之後,我決定就先從配置檔案載入講起,在逐步展開,配置檔案載入也是我們在使用 Spring 時遇到的第一個問題,今天就先來說說這個話題。

2.簡單的案例

先來一個簡單的案例,大家感受一下,然後我們順著案例講起。

首先我們建立一個普通的 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 配置檔案載入到記憶體中,再去解析它,再去。。。。。

一步一步來吧,先來看 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 檔案的解析。

如果小夥伴們覺得有收穫,記得點個在看鼓勵下鬆哥哦~

相關文章