在使用 Spring 的過程中,不知道大家有時候是否像我有一樣的疑問,都說 Spring 主要提供兩大機制:IoC 容器和 AOP 編程,而 IoC 容器是根本,提供控制反轉的功能,我們在使用的過程中只管聲明 bean 或使用註解的方式,IoC 容器就爲我們管理這些對象,並且幫我注入對象依賴,那麼這一切都是怎麼做到的呢?既然有這樣的疑問,那就得去弄明白,而想明白 IoC 容器的原理,首先就得需明白 Spring 是怎麼加載我們聲明的 bean,所以通過這篇文章來捋捋 Spring 加載 bean 的原理。
一個使用 Spring 較爲簡單的方式就是通過 ClassPathXmlApplicationContext 來啓動 Spring。
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring.xml");
}
接下來就探索下這樣簡單的一條語句背後下到底做了什麼事情。
/**
* Create a new ClassPathXmlApplicationContext, loading the definitions
* from the given XML file and automatically refreshing the context.
*/
public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
this(new String[] {configLocation}, true, null);
}
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {
// 調用父類 AbstractApplicationContext 的構造函數,設置父容器以及
// 初始化路勁解析策略: PathMatchingResourcePatternResolver
super(parent);
// 設置 configLocations,因爲有可能我們傳的路徑存在佔位符,需要解析,因此此時
// 會創建 Environment 對象,具體爲 StandardEvironment
setConfigLocations(configLocations);
// refresh 默認爲 true
if (refresh) {
// 刷新容器
refresh();
}
}
**PS:**可以得知默認父容器是爲 null
上面方法最核心的就是 refresh() 這行代碼了,所以看看你 AbstractApplicationContext 類的 refresh 具體做了哪些事情。
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 準備階段:初始化 PropertySource;驗證必須要的屬性是否存在
prepareRefresh();
// 委託子類刷新 BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
在只要加載 bean 定義的原理時,對 refresh() 方法內部對其他方法的調用可以先不深入瞭解,目前只需要對 AbstractRefreshableApplicationConttext 類的obtainFreshBeanFactory() 深入瞭解下,因爲這個方法內部會有一行代碼去做我們現在想要知道的事情。
/**
* This implementation performs an actual refresh of this context's underlying
* bean factory, shutting down the previous bean factory (if any) and
* initializing a fresh bean factory for the next phase of the context's lifecycle.
*/
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
// 對 BeanFactory 進行自定義配置,如是否可以重寫 bean 定義,是否允許循環引用
customizeBeanFactory(beanFactory);
// 加載 bean definition
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
可以看到,此方法除了創建 BeanFactory 外,還有一行代碼值得我們關注,那就是 loadBeanDefinitions(beanFactory),這行代碼做的事情就是去加載 bean 的定義。不知道大家有沒有想過 Spring 內部是怎麼表達我們在 xml 文件中聲明的 bean 的信息。沒錯,Spring 就是使用了 BeanDefinition 來對其進行表達的,類圖結構如下:
在對 BeanDefinition 類圖有個簡單瞭解下之後,我們看看 AbstractXmlApplicationContext 類的 loadBeanDefinitions() 方法。
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 爲指定的 BeanFactory 創建 XMLBeanDefinitionReader
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// 配置 BeanDefinitionReader
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
// 鉤子方法,讓子類有機會對 BeanDefinitionReader 進行定製化
initBeanDefinitionReader(beanDefinitionReader);
// 加載 BeanDefinition
loadBeanDefinitions(beanDefinitionReader);
}
可以看到其實此方法並沒有去加載 BeanDefinition,而是對 BeanDefinitionReader 進行設置和定製化,如果此方法名爲 initReader() 或者其他的可能更合適。那我們就在看看接下來的 loadBeanDefinitions() 又做了啥。
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
// 默認爲 null,子類可以重寫
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
// 獲取傳遞給 ClassPathXmlApplicationContext 構造函數的參數,即配置文件
String[] configLocations = getConfigLocations();
if (configLocations != null) {
// 讀取文件
reader.loadBeanDefinitions(configLocations);
}
}
此處還是沒有真正看到加載 BeanDefinition 的邏輯,還是在做準備階段,此時做的是獲取配置信息源,然後根據不同的來源調用不同的重載方法,我們就已其中最常見的一種形式說明。
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
Assert.notNull(locations, "Location array must not be null");
int counter = 0;
for (String location : locations) {
// 加載 BeanDefinition
counter += loadBeanDefinitions(location);
}
return counter;
}
因爲用戶可能爲了更好的組織信息,對不同的配置信息放在不同配置文件中,因此需要循環的去加載每個文件中的 BeanDefinition。
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {
// 默認爲使用的容器,此時是 ClassPathXmlApplicationnContext
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException(
"Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
}
if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
// 解析配置文件路徑爲 Resource
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
// 加載 BeanDefinition
int loadCount = loadBeanDefinitions(resources);
if (actualResources != null) {
for (Resource resource : resources) {
actualResources.add(resource);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
}
return loadCount;
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource pattern [" + location + "]", ex);
}
}
else {
// Can only load single resources by absolute URL.
Resource resource = resourceLoader.getResource(location);
int loadCount = loadBeanDefinitions(resource);
if (actualResources != null) {
actualResources.add(resource);
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");
}
return loadCount;
}
}
其實可以看到還是調用了參數爲 Resource 的 loadBeanDefinitions() 方法。那麼 Resource 到底是什麼呢?
Spring 的配置文件讀取是通過 ClassPathResource 進行封裝的,如 new ClassPathResource(“spring.xml”),那麼 ClassPathResource 完成了什麼功能呢?
在 java 中,將不同來源的資源抽象成 URI,通過註冊不同的 handler(URLStreamHandler)來處理不同來源的資源的讀取邏輯,一般 handler 的類型使用不同前綴(協議,Protocol)來識別,如 “file:”、“http:” 等,然而 URL 沒有默認定義相對 Classpath 或 ServletContext 等資源的 handler,雖然可以註冊自己的 URLStreamHandler 來解析特定的 URL 前綴(協議),然後這需要了解 URL 的實現機制,而且 URL 也沒有提供一些基本的方法,因而 Spring 對其內部使用到的資源實現了自己的抽象結構:Resource 接口來封裝底層資源。
public interface InputStreamSource { InputStream getInputStream() throws IOException; }
public interface Resource extends InputStreamSource { boolean exists(); boolean isReadable(); boolean isOpen(); URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String var1) throws IOException; String getFilename(); String getDescription(); }
有了 Resource 接口便可以對所有資源文件進行統一處理。
接下來看看 XMLBeanDefinitionReader 的 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 實現了 InputStreamSource,它是對 Resource 進行封裝,設置文件編碼。
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
// 獲取已經解析的資源
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
// 沒有就進行初始化
if (currentResources == null) {
currentResources = new HashSet<EncodedResource>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
// 如果當前資源已經被解析,就拋異常
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
// 跟 xml 文件對應的輸入流
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
// 把流封裝爲可以解析 xml 文件的 sax 輸入源
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 真正加載 BeanDefinition 的代碼入口
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();
}
}
}
敲黑板啦,終於看到了真正解析 BeanDefinition 的代碼入口了。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
// 獲取代表 xml 文件的 Document 對象(dom 解析 xml 文件)
Document doc = doLoadDocument(inputSource, resource);
// 解析 xml 文件並註冊 BeanDefinition
return registerBeanDefinitions(doc, resource);
} catch (Exception ignored) {
// 省略對各種異常的捕獲
}
}
到了這個地方纔可以說是把 Spring 對 BeanDefinition 的加載鏈路給捋清楚了,真正解析邏輯以及註冊留在下一篇博文。最後,用一張時序圖來總結下整個鏈路。