一、簡單案例引入
首先我們創建一個普通的 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 了。