Spring 原始碼第一篇開整!配置檔案是怎麼載入的?
上週把話撂出來,看起來小夥伴們都挺期待的,其實鬆哥也迫不及待想要開啟一個全新的系列。
但是目前的 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();
}
程式碼倒不難,我來稍微解釋下:
- 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 了。
4.小結
好啦,今天主要和小夥伴們分享一下 Spring 中的資源載入問題,這是容器啟動的起點,下篇文章我們來看 XML 檔案的解析。
如果小夥伴們覺得有收穫,記得點個在看鼓勵下鬆哥哦~
相關文章
- SpringBoot原始碼解析-配置檔案的載入Spring Boot原始碼
- Springboot 載入配置檔案原始碼分析Spring Boot原始碼
- Spring Boot @PropertySource 載入指定配置檔案、@ImportResource 匯入Spring 配置檔案Spring BootImport
- SpringBoot是如何載入配置檔案的?Spring Boot
- 2.2 spring5原始碼 -- ioc載入的整體流程Spring原始碼
- Spring 原始碼(3)Spring BeanFactory 是怎麼建立的?Spring原始碼Bean
- 精盡Spring Boot原始碼分析 - 配置載入Spring Boot原始碼
- Spring原始碼剖析開篇:什麼是Spring?Spring原始碼
- MyBatis載入配置檔案MyBatis
- Springboot載入配置檔案Spring Boot
- 微服務架構 | *2.3 Spring Cloud 啟動及載入配置檔案原始碼分析(以 Nacos 為例)微服務架構SpringCloud原始碼
- Spring Cloud Nacos實現動態配置載入的原始碼分析SpringCloud原始碼
- Profile配置和載入配置檔案
- Spring Boot第二彈,配置檔案怎麼造?Spring Boot
- Spring Cloud Alibaba基礎教程:Nacos配置的多檔案載入與共享配置SpringCloud
- xml是什麼格式的檔案 xml檔案怎麼開啟XML
- 原始碼解析Flask的配置檔案原始碼Flask
- JAVA載入配置檔案方法Java
- Spring原始碼之Bean的載入(二)Spring原始碼Bean
- Spring原始碼之Bean的載入(四)Spring原始碼Bean
- Spring原始碼之Bean的載入(三)Spring原始碼Bean
- Spring原始碼之Bean的載入(一)Spring原始碼Bean
- vsd格式檔案怎麼開啟 vsd是什麼格式的檔案,
- spring原始碼深度解析— IOC 之 開啟 bean 的載入Spring原始碼Bean
- 再讀Spring原始碼-Spring的啟動和載入Spring原始碼
- Spring原始碼系列:BeanDefinition載入(下)Spring原始碼Bean
- Spring 原始碼(4)在Spring配置檔案中自定義標籤如何實現?Spring原始碼
- bin是什麼檔案格式 bin檔案要怎麼開啟
- 原始碼解析Spring AOP的載入與生效原始碼Spring
- [springboot]配置檔案載入順序Spring Boot
- SpringBoot載入子模組配置檔案的方法Spring Boot
- lnk是什麼格式檔案 別人發的lnk檔案怎麼開啟
- .Net Core中的配置檔案原始碼解析原始碼
- Spring原始碼剖析3:Spring IOC容器的載入過程Spring原始碼
- mobi是什麼檔案格式 mobi檔案怎麼開啟檢視
- 轉載:為什麼在 Spring 的配置裡,最好不要配置 xsd 檔案的版本號Spring
- linux下為什麼每次修改完配置檔案之後都需要重新載入配置檔案Linux
- Spring原始碼:使用Thymeleaf生成PDF檔案Spring原始碼