摘要:本文結合《Spring源碼深度解析》來分析Spring 5.0.6版本的源代碼。若有描述錯誤之處,歡迎指正。
經過Spring源碼分析(二)容器基本用法和Spring源碼分析(三)容器核心類兩篇文章,我們已經對Spring的容器功能有了一個大致的了解,盡管你可能還很迷糊,但是不要緊,接下來我們會詳細探索每個步驟的實現。首先要深入分析的是以下功能的代碼實現:
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring/spring-test.xml"));
通過XmlBeanFactory初始化時序圖,我們看下上面代碼的執行邏輯:
時序圖從BeanFactoryTest測試類開始,通過時序圖我們可以一目了然地看到整個邏輯處理順序。先調用了ClassPathResource的構造函數來構造Resource資源文件的實例對象,后續的資源處理就可以用Resource提供的各種服務來操作了,當我們有了Resource后就可以進行XmlBeanFactory的初始化了。那么Resource文件是如何封裝的呢?
1. 配置文件封裝
Spring的配置文件讀取是通過ClassPathResource進行封裝的,如new ClassPathResource("spring/spring-test.xml"),那么ClassPathResource完成了什么功能呢?
在Java中,將不同來源的資源抽象成URL,通過注冊不同的handler(URLStreamHander)來處理不同來源的資源的讀取邏輯,一般handler的類型使用不同的前綴(協議,Protocol)來識別,如“file:”、"http:"、"jar:"等,然而URL沒有默認定義相對Classpath或ServletContext等資源的handler,雖然可以注冊自己的URLStreamHandler來解析特定的URL前綴(協議),比如“classpath:”,然而這需要了解URL的實現機制,而且URL也沒有提供一些基本的方法,如檢查當前資源是否存在、檢查當前資源是否可讀等方法。因而Spring對其內部使用到的資源實現了自己的抽象結構:Resource接口來封裝底層資源。
public interface InputStreamSource { InputStream getInputStream() throws IOException; }
public interface Resource extends InputStreamSource { boolean exists(); default boolean isReadable() { return true; } 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封裝任何能返回InputStream的類,比如File、Classpath下的資源和Byte Array等。它只有一個方法定義:getInputStream(),該方法返回一個新的InputStream對象。
Resource接口抽象了所有Spring內部使用到的底層資源:File、URL、Classpath等。首先,它定義了3個判斷當前資源狀態的方法:存在性(exists)、可讀性(isReadable)、是否處於打開狀態(isOpen)。另外,Resource接口還提供了不同資源到URL、URI、File類型的轉換,以及獲取lastModified屬性、文件名(不帶文件信息的文件名,getFilename())的方法。為了便於操作,Resource還提供了基於當前資源創建一個相對資源的方法:createRelative()。在錯誤處理中需要詳細地打印出錯的資源文件,因而Resource還提供了getDescription()方法用於在錯誤處理中的打印信息。
對不同來源的資源文件都有相應的Resource實現:文件(FileSystemResource)、Classpath資源(ClasspathResource)、URL資源(URLResource)、InputStream資源(InputStreamResource)、Byte數組(ByteArrayResource)等。相關類圖如下圖所示:
在日常的開發工作中,資源文件的加載也是經常用到的,可以直接使用Spring提供的類,比如在希望加載文件時可以使用以下代碼:
Resource resource = new ClassPathResource("spring/spring-test.xml.xml"); InputStream inputStream = resource.getInputStream();
得到inputStream后,我們可以按照以前的開發方式進行實現了,並且我們已經可以利用Resource及其子類為我們提供好的諸多特性。
有了Resource接口便可以對所有資源文件進行統一處理。至於實現,其實是非常簡單的,以getInputStream為例,ClassPathResource中的實現方式便是通過class或者classLoader提供的底層方法進行調用,而對於FileSystemResource的實現其實更簡單,直接使用FileInputStream對文件進行實例化。
ClassPathResource.java
/** * This implementation opens an InputStream for the given class path resource. * @see java.lang.ClassLoader#getResourceAsStream(String) * @see java.lang.Class#getResourceAsStream(String) */ @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; }
FileSystemResource.java
/** * This implementation opens a NIO file stream for the underlying file. * @see java.io.FileInputStream */ @Override public InputStream getInputStream() throws IOException { try { return Files.newInputStream(this.file.toPath()); } catch (NoSuchFileException ex) { throw new FileNotFoundException(ex.getMessage()); } }
當通過Resource相關類完成了對配置文件進行封裝后配置文件的讀取工作就全權交給XmlBeanDefinitionReader來處理了。
了解了Spring中將配置文件封裝為Resource類型的實例方法后,我們就可以繼續探討XmlBeanFactory的初始化方法了,XmlBeanFactory初始化有許多方法,Spring中提供了很多的構造函數,在這里分析的是使用Resource實例作為構造函數參數的方法,代碼如下:
/** * Create a new XmlBeanFactory with the given resource, * which must be parsable using DOM. * @param resource the XML resource to load bean definitions from * @throws BeansException in case of loading or parsing errors */ public XmlBeanFactory(Resource resource) throws BeansException { // 調用XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory)構造方法 this(resource, null); } /** * Create a new XmlBeanFactory with the given input stream, * which must be parsable using DOM. * @param resource the XML resource to load bean definitions from * @param parentBeanFactory parent bean factory * @throws BeansException in case of loading or parsing errors */ // parentBeanFactory為父類BeanFactory用於factory合並,可以為空 public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); }
上面函數的代碼中,this.reader.loadBeanDefinitions(resource)才是資源加載的真正實現,也是我們分析的重點之一。我們可以看到時序圖中提到的XmlBeanDefinitionReader加載數據就是這里完成的,但是在XmlBeanDefinitionReader加載數據前還有一個調用父類構造函數初始化的過程:super(parentBeanFactory),跟蹤代碼到父類AbstractAutowireCapableBeanfactory的構造函數中:
/** * Create a new AbstractAutowireCapableBeanFactory. */ public AbstractAutowireCapableBeanFactory() { super(); ignoreDependencyInterface(BeanNameAware.class); ignoreDependencyInterface(BeanFactoryAware.class); ignoreDependencyInterface(BeanClassLoaderAware.class); }
這里有必要提及下ignoreDependencyInterface方法。ignoreDependencyInterface的主要功能,是忽略給定接口的自動裝配功能,那么,這樣做的目的是什么呢?會產生什么樣的效果呢?
舉例來說,當A中有屬性B,那么當Spring在獲取A的Bean的如果其屬性B還沒有初始化,那么Spring會自動初始化B,這也是Spring提供的一個重要特性。但是,某些情況下B不會被初始化,其中的一種情況就是B實現了BeanNameAware接口。Spring中是這樣介紹的,自動裝配的時候,忽略給定的依賴接口,典型的應用是通過其他方式解析Application上下文注冊依賴,類似於BeanFactory通過BeanFactoryAware進行注入或者ApplicationContext通過ApplicationContextAware進行注入。
2. 加載Bean
之前提到的在XmlBeanFactory構造函數中調用了XmlBeanDefinitionReader類型的reader屬性提供的方法this.reader.loadBeanDefinitions(resource),而這句代碼則是整個資源加載的切入點,我們先來看看這個方法的時序圖,如下圖所示:
看到上圖我們才知道,原來繞了這么久還沒有切入正題,還一直在為加載XML文件和解析注冊Bean在做准備工作。從上面的時序圖中我們嘗試梳理整個的處理過程如下:
- 封裝資源文件。當進入XmlBeanDefinitionReader后首先對參數Resource使用EncodeResource類進行封裝。
- 獲取輸入流。從Resource中獲取對應的InputStream並構造InputSource。
- 通過構造的InputSource實例和Resource實例繼續調用函數doLoadBeanDefinitions。
我們來看一下loadBeanDefinitions函數具體的實現過程:
/** * Load bean definitions from the specified XML file. * @param resource the resource descriptor for the XML file * @return the number of bean definitions found * @throws BeanDefinitionStoreException in case of loading or parsing errors */ @Override public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { return loadBeanDefinitions(new EncodedResource(resource)); }
那么EncodeResource的作用是什么呢?通過名稱,我們可以大致推斷這個類主要是用於對資源文件的編碼進行處理。其中的主要邏輯體現在getReader()方法中,當設置了編碼屬性的時候Spring會使用相應的編碼作為輸入流的編碼。
/** * Open a {@code java.io.Reader} for the specified resource, using the specified * {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding} * (if any). * @throws IOException if opening the Reader failed * @see #requiresReader() * @see #getInputStream() */ 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()); } }
上面代碼構造了一個有編碼(encoding)的InputStreamReader。當構造好encodeResource對象后,再次轉入了可復用方法loadBeanDefinitions(new EncodedResource(resource))。
這個方法內部才是真正的數據准備階段,也就是時序圖鎖描述的邏輯:
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); if (logger.isDebugEnabled()) { logger.debug("Loading XML bean definitions from " + encodedResource.getResource()); } // 通過屬性來記錄已經加載的資源 Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { currentResources = new HashSet<>(4); this.resourcesCurrentlyBeingLoaded.set(currentResources); } if (!currentResources.add(encodedResource)) { throw new BeanDefinitionStoreException( "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); } try { // 從encodedResource中獲取已經封裝的Resource對象並再次從Resource中獲取其中的inputStream InputStream inputStream = encodedResource.getResource().getInputStream(); try { // InputSource這個類並不是來自於Spring,他的全路徑是org.xml.sax.InputSource InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } // 真正進入了邏輯核心部分 return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); } finally { // 關閉輸入流 inputStream.close(); } } catch (IOException ex) { throw new BeanDefinitionStoreException( "IOException parsing XML document from " + encodedResource.getResource(), ex); } finally { currentResources.remove(encodedResource); if (currentResources.isEmpty()) { this.resourcesCurrentlyBeingLoaded.remove(); } } }
我們再次准備一下數據准備階段的邏輯,首先對傳入的resource參數做封裝,目的是考慮到Resource可能存在編碼要求的情況,其次,通過SAX讀取XML文件的方式來准備InputSource對象,最后將准備的數據通過參數傳入真正的核心處理部分doLoadBeanDefinitions(inputSource, encodedResource.getResource())。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { Document doc = doLoadDocument(inputSource, resource); return registerBeanDefinitions(doc, resource); } catch (BeanDefinitionStoreException ex) { throw ex; } catch (SAXParseException ex) { throw new XmlBeanDefinitionStoreException(resource.getDescription(), "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex); } catch (SAXException ex) { throw new XmlBeanDefinitionStoreException(resource.getDescription(), "XML document from " + resource + " is invalid", ex); } catch (ParserConfigurationException ex) { throw new BeanDefinitionStoreException(resource.getDescription(), "Parser configuration exception parsing XML from " + resource, ex); } catch (IOException ex) { throw new BeanDefinitionStoreException(resource.getDescription(), "IOException parsing XML document from " + resource, ex); } catch (Throwable ex) { throw new BeanDefinitionStoreException(resource.getDescription(), "Unexpected exception parsing XML document from " + resource, ex); } }
上面的代碼只做了兩件事,每一件都是必不可少的。
- 加載XML文件,並得到對應的Document。
- 根據返回的Document注冊Bean信息。
這兩個步驟支撐着整個Spring容器部分的實現基礎,尤其是第二部對配置文件的解析,邏輯非常復雜,下一節里面先從獲取Document講起。