上周把話撂出來,看起來小伙伴們都挺期待的,其實松哥也迫不及待想要開啟一個全新的系列。
但是目前的 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 文件的解析。
如果小伙伴們覺得有收獲,記得點個在看鼓勵下松哥哦~