本文已收錄 【修煉內功】躍遷之路
閱讀源碼是一件極其枯燥無比的事情,對於使用頻率較高的組件,若是能作到知其然且知其因此然,這對平常工做中不管是問題排查、代碼優化、功能擴展等都是利大於弊的,如同老司機開車(對,就是開車),會讓你有一種參與感,而不只僅把它當成一種工具,若能習之精髓、學以至用,那便再好不過!html
從工做之初便開始接觸Spring框架,時至今日也沒有認真地正視過它的實現細節,今日開拔,但願可以堅持下來~java
對於Spring如此「龐大」(至少與我而言)的框架,不想一上來就將level提的很高,以上帝的視角將整個Spring框架的架構圖或者類圖之類拋出來,對於並不特別瞭解的人來講,除了膜拜Spring的「宏偉」以外別無他法,依然不清楚應該如何下手git
這裏,但願可以按部就班,將Spring的幾個核心組件各個擊破,再將各組件串聯起來,以點至面github
Spring系列文章web
- 默認您對Spring框架有必定的瞭解及使用經驗
- 既然是源碼分析便不可避免地會貼一些源碼,會盡可能以精簡代碼、僞代碼、額外註釋等方式呈現,以減小源碼所佔的篇幅
言歸正傳,本篇就Spring的資源Resource聊起spring
Resource(資源)是進入Spring生態的第一道門,不敢說它是Spring的基石,但絕對是Spring的核心組件之一segmentfault
Resource主要負責資源的(讀寫)操做,最爲常見地出如今系統各初始化階段,如網絡
ApplicationContext
new ClassPathXmlApplicationContext("classpath:spring/application.xml");
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/application.xml</param-value> </context-param>
@Configuration @ComponentScan("com.manerfan") public class AppConfiguration {}
<context:component-scan base-package="com.manerfan"></context:component-scan>
mybatis.config-location=classpath:/mybatis/mybatis-config.xml
@Component public class SomeComponent { @Value("classpath:in18/zh-cn.properties") private Resource in18ZhCn; }
<bean id="someComponent" class="...SomeComponent"> <property name="in18ZhCn" value="classpath:in18/zh-cn.properties"/> </bean>
application[-env].properties
application[-env].yml
,加載META-INF
配置文件等Java中的URL
經過不一樣的前綴(協議)已經實現了一套資源的讀取,如磁盤文件file:///var/log/system.log
、網絡文件https://some.host/some.file.txt
甚至jar中的classjar:file:///spring-core.jar!/org/springframework/core/io/Resource.class
,然而Spring並無採用URL
的方案,其官方文檔給出了必定的解釋mybatis
https://docs.spring.io/spring...Java’s standard
java.net.URL
class and standard handlers for various URL prefixes, unfortunately, are not quite adequate enough for all access to low-level resources. For example, there is no standardizedURL
implementation that may be used to access a resource that needs to be obtained from the classpath or relative to aServletContext
. While it is possible to register new handlers for specializedURL
prefixes (similar to existing handlers for prefixes such ashttp:
), this is generally quite complicated, and theURL
interface still lacks some desirable functionality, such as a method to check for the existence of the resource being pointed to.架構
其一,URL
擴展複雜;其二,URL
功能有限
Spring將不一樣類型的資源統一抽象成了Resource
,這有點相似Linux系統的「一切皆文件」(磁盤文件、目錄、硬件設備、套接字、網絡等),資源的抽象屏蔽了不一樣類型資源的差別性,統一了操做接口
⇪Resource
的定義很是簡潔明瞭,方法的命名已經足夠清晰,再也不統一解釋
public interface Resource extends InputStreamSource { boolean exists(); boolean isReadable(); boolean isOpen(); boolean isFile() URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; ReadableByteChannel readableChannel() throws IOException; long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String relativePath) throws IOException; String getFilename(); String getDescription(); // ... }
Resource
繼承自更爲抽象的⇪InputStreamSource
public interface InputStreamSource { String CLASSPATH_URL_PREFIX = "classpath:"; InputStream getInputStream() throws IOException; }
其只有一個方法getInputStream
,用於獲取資源的InputStream
對於Resouce
的具體實現可參考下圖(莫被錯綜複雜的類關係擾亂了思路),一層層剝離解析
⇪WritableResource
派生自Resource
,其在Resource
的基礎上增長了'寫'相關的能力
public interface WritableResource extends Resource { boolean isWritable(); OutputStream getOutputStream() throws IOException; WritableByteChannel writableChannel() throws IOException; }
這裏重點關注Resource
'讀'能力的實現
⇪AbstractResource
實現了大部分Resource
中公共的、無底層差別的邏輯,實現較爲簡單,再也不詳述
AbstractResource
的具體實現類則是封裝了不一樣類型的資源類庫,使用具體的類庫函數實現Resource
定義的一系列接口
⇪FileSystemResource
封裝了java.io.File
(或java.io.Path
)的能力實現了Resource
的一些細節
public class FileSystemResource extends AbstractResource implements WritableResource { @Override public InputStream getInputStream() throws IOException { // ... // 將File/Path封裝爲InputStream return Files.newInputStream(this.filePath); // ... } @Override public OutputStream getOutputStream() throws IOException { // 將File/Path封裝爲OutputStream return Files.newOutputStream(this.filePath); } @Override public URL getURL() throws IOException { return (this.file != null ? this.file.toURI().toURL() : this.filePath.toUri().toURL()); } @Override public URI getURI() throws IOException { return (this.file != null ? this.file.toURI() : this.filePath.toUri()); } // ... }
同理,⇪ByteArrayResource
封裝了ByteArray
的能力,⇪InputStreamResource
封裝了InputSream
的能力,等等,再也不一一介紹
⇪AbstractFileResolvingResource
則把中心放在java.net.URL
上,其使用URL
的能力重寫了其父類AbstractResource
的大部分實現
AbstractFileResolvingResource
的實現類只有兩個,⇪UrlResource
及⇪ClassPathResource
UrlResource
一樣簡單地封裝了URL
的能力來實現Resource
中定義的接口
public class UrlResource extends AbstractFileResolvingResource { @Override public InputStream getInputStream() throws IOException { URLConnection con = this.url.openConnection(); ResourceUtils.useCachesIfNecessary(con); try { return con.getInputStream(); } catch (IOException ex) { // Close the HTTP connection (if applicable). if (con instanceof HttpURLConnection) { ((HttpURLConnection) con).disconnect(); } throw ex; } } // ... }
ClassPathResource
則是藉助ClassLoader
的能力來實現Resource
中定義的接口
public class ClassPathResource extends AbstractFileResolvingResource { @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; } }
細心的可能會發現,這裏還有一個⇪EncodedResource
,其在Resource
的基礎上加入了編碼信息,並提供了額外的getReader
接口
public class EncodedResource implements InputStreamSource { 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()); } } }
這對於獲取編碼格式有要求的資源來說十分受用
@Component public class MyComponent { private final Properties properties; public MyComponent(@Value("classpath:/config/my-config.properties") Resource resource) { // Properties讀取配置默認編碼爲ISO-8859-1 this.properties = PropertiesLoaderUtils.loadProperties(new EncodedResource(resource, "utf-8")); } // ... }
⇪Resource
將資源的操做抽象,屏蔽不一樣類型資源差別性,統一操做接口⇪FileSystemResource
、⇪ByteArrayResource
、⇪InputStreamResource
、⇪UrlResource
及⇪ClassPathResource
等,藉助相應的資源類庫能力,實現Resource
中定義的接口
FileSystemResource
→ File
or Path
ByteArrayResource
→ ByteArray
InputStreamResource
→ InputStream
UrlResource
→ Url
ClassPathResource
→ ClassLoader
使用上,針對不一樣的資源類型建立不一樣的Resource
便可
new FileSystemResource("/var/log/system.log"); // 文件系統中的文件 new ClassPathResource("/config/my-config.properties"); // classpath中的文件 new UrlResource("http://oss.manerfan.com/config/my-config.properties"); // 網絡上的文件
「針對不一樣的資源類型建立不一樣的Resource
」,如上例中的硬編碼並不符合開閉原則,對於開發者來講其實並不那麼友好,還好Spring提供了⇪ResourceLoader
(接下來,你會發現Spring中提供了各類各樣的Loader、Resolver、Aware等等)
public interface ResourceLoader { Resource getResource(String location); }
ResourceLoader
中定義了getResource
方法用於建立合適類型的Resource
,至於應該建立哪一種類型以及如何建立,則交由ResourceLoader
處理
ResourceLoader
的實現類主要有兩種,其一爲⇪DefaultResourceLoader
,其二爲⇪PathMatchingResourcePatternResolver
⇪DefaultResourceLoader
爲ResourceLoader
的默認實現,其實現極爲簡單
public class DefaultResourceLoader implements ResourceLoader { @Override public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); // 1. 若是存在自定義的解析器,優先使用自定義解析器 for (ProtocolResolver protocolResolver : getProtocolResolvers()) { Resource resource = protocolResolver.resolve(location, this); if (resource != null) { return resource; } } // 2. 若是沒有自定義解析器,或者自定義解析器沒法解析,則使用默認實現 if (location.startsWith("/")) { // 構造ClassPathResource return getResourceByPath(location); } else if (location.startsWith(CLASSPATH_URL_PREFIX)) { // 取"classpath:"後的內容,構造ClassPathResource return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { // 嘗試構造URLResource URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // 降級到ClassPathResource return getResourceByPath(location); } } } protected Resource getResourceByPath(String path) { return new ClassPathContextResource(path, getClassLoader()); } }
首先會嘗試使用自定義解析器解析(經過addProtocolResolver
方法添加),若是沒有或者解析失敗纔會使用默認實現
默認實現邏輯中,若是是以/
或classpath:
開頭的,會直接構造ClassPathResource
,不然會嘗試構造爲UrlResource
瞭解ClassPathResource
實現的會注意到,若是在全部的classpath路徑中同時存在多個文件匹配(如,同時在a.jar及b.jar中存在/config/my-config.properties文件),則只會返回首個匹配到的(取決於JVM加載順序),這也是有別於PathMatchingResourcePatternResolver
的一個地方
⇪PathMatchingResourcePatternResolver
實現自⇪ResourcePatternResolver
接口
public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; Resource[] getResources(String locationPattern) throws IOException; }
ResourcePatternResolver
在ResourceLoader
的基礎上,增長了批量獲取的接口getResources
默認狀況下,PathMatchingResourcePatternResolver
的getResource
實現實際上是使用了DefaultResourceLoader
(固然你也能夠本身指定默認的ResourceLoader
實現)
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver { public PathMatchingResourcePatternResolver() { this.resourceLoader = new DefaultResourceLoader(); } @Override public Resource getResource(String location) { return getResourceLoader().getResource(location); } }
PathMatchingResourcePatternResolver
的一大特色在於PathMatching(默認使用AntPathMatcher
,也能夠指定),對於相似classpath:/config/my-*.properties或classpath*:/config/my-*.properties等Ant風格的資源進行匹配,其基本思路大體爲
⇪findPathMatchingResources
方法)classpath:與classpath*:的區別主要在於父目錄的查找邏輯
父目錄的查找藉助DefaultResourceLoader
的能力(歸根結底使用了ClassLoader.getResource
),上文也有提到,這裏只會返回首個匹配到的目錄資源
@Override public Resource[] getResources(String locationPattern) throws IOException { // ... 各類 if-else 以後 // a single resource with the given name return new Resource[] {getResourceLoader().getResource(locationPattern)}; }
父目錄的查找則直接使用ClassLoader.getResources
,返回全部classpath中的目錄資源
@Override public Resource[] getResources(String locationPattern) throws IOException { // ... 各類 if-else 以後 // all class path resources with the given name return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } protected Resource[] findAllClassPathResources(String location) throws IOException { // ... Set<Resource> result = doFindAllClassPathResources(path); // ... } protected Set<Resource> doFindAllClassPathResources(String path) throws IOException { // ... Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); // ... }
因此,classpath:/config/my-*.properties只會返回首個匹配到的/config/目錄中全部的my-*.properties資源,而classpath*:/config/my-*.properties則會返回全部匹配到的/config/目錄中全部的my-*.properties資源
AbstractApplicationContext
同時實現了ResourcePatternResolver
接口並繼承了DefaultResourceLoader
,
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { public AbstractApplicationContext() { this.resourcePatternResolver = getResourcePatternResolver(); } protected ResourcePatternResolver getResourcePatternResolver() { return new PathMatchingResourcePatternResolver(this); } }
因此,如開篇的幾個例子裏,在ApplicationContext
中獲取Resource
資源,大多數狀況下使用的都是PathMatchingResourcePatternResolver
ResourceLoader
根據不一樣的前綴(協議)生成相對應的Resource
(Spring中提供了各類各樣的Loader、Resolver、Aware等等)DefaultResourceLoader
只能獲取單個資源,且只能獲取classpath中首次匹配到的資源(ClassLoader.getResource
)PathMatchingResourcePatternResolver
可使用Ant風格匹配並返回多個資源
ClassLoader.getResource
)全部的知足給定Ant規則的資源ClassLoader.getResources
)全部的知足給定Ant規則的資源ApplicationContext
默認使用PathMatchingResourcePatternResolver
獲取Resource
資源⇪Resource
將資源的操做抽象,屏蔽不一樣類型資源差別性,統一操做接口Resource
的不一樣實現,均藉助相應的資源類庫能力,來實現Resource
中定義的接口ResourceLoader
根據不一樣的前綴(協議)生成相對應的Resource
DefaultResourceLoader
只能獲取單個資源,且只能獲取classpath中首次匹配到的資源PathMatchingResourcePatternResolver
可使用Ant風格匹配並返回多個資源,classpath:與classpath*:的區別在於如何獲取根目錄以在其中查找匹配Ant風格的資源ApplicationContext
默認使用PathMatchingResourcePatternResolver
獲取Resource
資源