在spring源碼深度解析— IOC 之 默認標簽解析(上)中我們已經完成了從xml配置文件到BeanDefinition的轉換,轉換后的實例是GenericBeanDefinition的實例。本文主要來看看標簽解析剩余部分及BeanDefinition的注冊。
默認標簽中的自定義標簽解析
在上篇博文中我們已經分析了對於默認標簽的解析,我們繼續看戲之前的代碼,如下圖片中有一個方法:delegate.decorateBeanDefinitionIfRequired(ele, bdHolder)
這個方法的作用是什么呢?首先我們看下這種場景,如下配置文件:
<bean id="demo" class="com.chenhao.spring.MyTestBean">
<property name="beanName" value="bean demo1"/>
<meta key="demo" value="demo"/>
<mybean:username="mybean"/>
</bean>
這個配置文件中有個自定義的標簽,decorateBeanDefinitionIfRequired方法就是用來處理這種情況的,其中的null是用來傳遞父級BeanDefinition的,我們進入到其方法體:
public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder) { return decorateBeanDefinitionIfRequired(ele, definitionHolder, null); } public BeanDefinitionHolder decorateBeanDefinitionIfRequired( Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) { BeanDefinitionHolder finalDefinition = definitionHolder; // Decorate based on custom attributes first. NamedNodeMap attributes = ele.getAttributes(); for (int i = 0; i < attributes.getLength(); i++) { Node node = attributes.item(i); finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); } // Decorate based on custom nested elements. NodeList children = ele.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (node.getNodeType() == Node.ELEMENT_NODE) { finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); } } return finalDefinition; }
我們看到上面的代碼有兩個遍歷操作,一個是用於對所有的屬性進行遍歷處理,另一個是對所有的子節點進行處理,兩個遍歷操作都用到了decorateIfRequired(node, finalDefinition, containingBd);方法,我們繼續跟蹤代碼,進入方法體:
public BeanDefinitionHolder decorateIfRequired( Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) { // 獲取自定義標簽的命名空間 String namespaceUri = getNamespaceURI(node); // 過濾掉默認命名標簽 if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) { // 獲取相應的處理器 NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); if (handler != null) { // 進行裝飾處理 BeanDefinitionHolder decorated = handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd)); if (decorated != null) { return decorated; } } else if (namespaceUri.startsWith("http://www.springframework.org/")) { error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", node); } else { if (logger.isDebugEnabled()) { logger.debug("No Spring NamespaceHandler found for XML schema namespace [" + namespaceUri + "]"); } } } return originalDef; } public String getNamespaceURI(Node node) { return node.getNamespaceURI(); } public boolean isDefaultNamespace(@Nullable String namespaceUri) { //BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans"; return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri)); }
首先獲取自定義標簽的命名空間,如果不是默認的命名空間則根據該命名空間獲取相應的處理器,最后調用處理器的 decorate()
進行裝飾處理。具體的裝飾過程這里不進行講述,在后面分析自定義標簽時會做詳細說明。
注冊解析的BeanDefinition
對於配置文件,解析和裝飾完成之后,對於得到的beanDefinition已經可以滿足后續的使用要求了,還剩下注冊,也就是processBeanDefinition函數中的BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder,getReaderContext().getRegistry())代碼的解析了。進入方法體:
public static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { // Register bean definition under primary name. //使用beanName做唯一標識注冊 String beanName = definitionHolder.getBeanName(); registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); // Register aliases for bean name, if any. //注冊所有的別名 String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } }
從上面的代碼我們看到是用了beanName作為唯一標示進行注冊的,然后注冊了所有的別名aliase。而beanDefinition最終都是注冊到BeanDefinitionRegistry中,接下來我們具體看下注冊流程。
通過beanName注冊BeanDefinition
在spring中除了使用beanName作為key將BeanDefinition放入Map中還做了其他一些事情,我們看下方法registerBeanDefinition代碼,BeanDefinitionRegistry是一個接口,他有三個實現類,DefaultListableBeanFactory、SimpleBeanDefinitionRegistry、GenericApplicationContext,其中SimpleBeanDefinitionRegistry非常簡單,而GenericApplicationContext最終也是使用的DefaultListableBeanFactory中的實現方法,我們看下代碼:
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException { // 校驗 beanName 與 beanDefinition Assert.hasText(beanName, "Bean name must not be empty"); Assert.notNull(beanDefinition, "BeanDefinition must not be null"); if (beanDefinition instanceof AbstractBeanDefinition) { try { // 校驗 BeanDefinition // 這是注冊前的最后一次校驗了,主要是對屬性 methodOverrides 進行校驗 ((AbstractBeanDefinition) beanDefinition).validate(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", ex); } } BeanDefinition oldBeanDefinition; // 從緩存中獲取指定 beanName 的 BeanDefinition oldBeanDefinition = this.beanDefinitionMap.get(beanName); /** * 如果存在 */ if (oldBeanDefinition != null) { // 如果存在但是不允許覆蓋,拋出異常 if (!isAllowBeanDefinitionOverriding()) { throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName + "': There is already [" + oldBeanDefinition + "] bound."); } // else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) { // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE if (this.logger.isWarnEnabled()) { this.logger.warn("Overriding user-defined bean definition for bean '" + beanName + "' with a framework-generated bean definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } // 覆蓋 beanDefinition 與 被覆蓋的 beanDefinition 不是同類 else if (!beanDefinition.equals(oldBeanDefinition)) { if (this.logger.isInfoEnabled()) { this.logger.info("Overriding bean definition for bean '" + beanName + "' with a different definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Overriding bean definition for bean '" + beanName + "' with an equivalent definition: replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]"); } } // 允許覆蓋,直接覆蓋原有的 BeanDefinition this.beanDefinitionMap.put(beanName, beanDefinition); } /** * 不存在 */ else { // 檢測創建 Bean 階段是否已經開啟,如果開啟了則需要對 beanDefinitionMap 進行並發控制 if (hasBeanCreationStarted()) { // beanDefinitionMap 為全局變量,避免並發情況 synchronized (this.beanDefinitionMap) { // this.beanDefinitionMap.put(beanName, beanDefinition); List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1); updatedDefinitions.addAll(this.beanDefinitionNames); updatedDefinitions.add(beanName); this.beanDefinitionNames = updatedDefinitions; if (this.manualSingletonNames.contains(beanName)) { Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames); updatedSingletons.remove(beanName); this.manualSingletonNames = updatedSingletons; } } } else { // 不會存在並發情況,直接設置 this.beanDefinitionMap.put(beanName, beanDefinition); this.beanDefinitionNames.add(beanName); this.manualSingletonNames.remove(beanName); } this.frozenBeanDefinitionNames = null; } if (oldBeanDefinition != null || containsSingleton(beanName)) { // 重新設置 beanName 對應的緩存 resetBeanDefinition(beanName); } }
處理過程如下:
- 首先 BeanDefinition 進行校驗,該校驗也是注冊過程中的最后一次校驗了,主要是對 AbstractBeanDefinition 的 methodOverrides 屬性進行校驗
- 根據 beanName 從緩存中獲取 BeanDefinition,如果緩存中存在,則根據 allowBeanDefinitionOverriding 標志來判斷是否允許覆蓋,如果允許則直接覆蓋,否則拋出 BeanDefinitionStoreException 異常
- 若緩存中沒有指定 beanName 的 BeanDefinition,則判斷當前階段是否已經開始了 Bean 的創建階段(),如果是,則需要對 beanDefinitionMap 進行加鎖控制並發問題,否則直接設置即可。對於
hasBeanCreationStarted()
方法后續做詳細介紹,這里不過多闡述。 - 若緩存中存在該 beanName 或者 單利 bean 集合中存在該 beanName,則調用
resetBeanDefinition()
重置 BeanDefinition 緩存。
其實整段代碼的核心就在於 this.beanDefinitionMap.put(beanName, beanDefinition);
。BeanDefinition 的緩存也不是神奇的東西,就是定義 一個 ConcurrentHashMap,key 為 beanName,value 為 BeanDefinition。
通過別名注冊BeanDefinition
通過別名注冊BeanDefinition最終是在SimpleBeanDefinitionRegistry中實現的,我們看下代碼:
public void registerAlias(String name, String alias) { Assert.hasText(name, "'name' must not be empty"); Assert.hasText(alias, "'alias' must not be empty"); synchronized (this.aliasMap) { if (alias.equals(name)) { this.aliasMap.remove(alias); } else { String registeredName = this.aliasMap.get(alias); if (registeredName != null) { if (registeredName.equals(name)) { // An existing alias - no need to re-register return; } if (!allowAliasOverriding()) { throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" + name + "': It is already registered for name '" + registeredName + "'."); } } //當A->B存在時,若再次出現A->C->B時候則會拋出異常。 checkForAliasCircle(name, alias); this.aliasMap.put(alias, name); } } }
上述代碼的流程總結如下:
(1)alias與beanName相同情況處理,若alias與beanName並名稱相同則不需要處理並刪除原有的alias
(2)alias覆蓋處理。若aliasName已經使用並已經指向了另一beanName則需要用戶的設置進行處理
(3)alias循環檢查,當A->B存在時,若再次出現A->C->B時候則會拋出異常。
alias標簽的解析
對應bean標簽的解析是最核心的功能,對於alias、import、beans標簽的解析都是基於bean標簽解析的,接下來我就分析下alias標簽的解析。我們回到 parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate)方法,繼續看下方法體,如下圖所示:
對bean進行定義時,除了用id來 指定名稱外,為了提供多個名稱,可以使用alias標簽來指定。而所有這些名稱都指向同一個bean。在XML配置文件中,可用單獨的元素來完成bean別名的定義。我們可以直接使用bean標簽中的name屬性,如下:
<bean id="demo" class="com.chenhao.spring.MyTestBean" name="demo1,demo2">
<property name="beanName" value="bean demo1"/>
</bean>
在Spring還有另外一種聲明別名的方式:
<bean id="myTestBean" class="com.chenhao.spring.MyTestBean"/>
<alias name="myTestBean" alias="testBean1,testBean2"/>
我們具體看下alias標簽的解析過程,解析使用的方法processAliasRegistration(ele),方法體如下:
protected void processAliasRegistration(Element ele) { //獲取beanName String name = ele.getAttribute(NAME_ATTRIBUTE); //獲取alias String alias = ele.getAttribute(ALIAS_ATTRIBUTE); boolean valid = true; if (!StringUtils.hasText(name)) { getReaderContext().error("Name must not be empty", ele); valid = false; } if (!StringUtils.hasText(alias)) { getReaderContext().error("Alias must not be empty", ele); valid = false; } if (valid) { try { //注冊alias getReaderContext().getRegistry().registerAlias(name, alias); } catch (Exception ex) { getReaderContext().error("Failed to register alias '" + alias + "' for bean with name '" + name + "'", ele, ex); } getReaderContext().fireAliasRegistered(name, alias, extractSource(ele)); } }
通過代碼可以發現解析流程與bean中的alias解析大同小異,都是講beanName與別名alias組成一對注冊到registry中。跟蹤代碼最終使用了SimpleAliasRegistry中的registerAlias(String name, String alias)方法
import標簽的解析
對於Spring配置文件的編寫,經歷過大型項目的人都知道,里面有太多的配置文件了。基本采用的方式都是分模塊,分模塊的方式很多,使用import就是其中一種,例如我們可以構造這樣的Spring配置文件:
<?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 id="demo" class="com.chenhao.spring.MyTestBean" name="demo1,demo2"> <property name="beanName" value="bean demo1"/> </bean> <import resource="lookup-method.xml"/> <import resource="replaced-method.xml"/> </beans>
applicationContext.xml文件中使用import方式導入有模塊配置文件,以后若有新模塊的加入,那就可以簡單修改這個文件了。這樣大大簡化了配置后期維護的復雜度,並使配置模塊化,易於管理。我們來看看Spring是如何解析import配置文件的呢。解析import標簽使用的是importBeanDefinitionResource(ele),進入方法體:
protected void importBeanDefinitionResource(Element ele) { // 獲取 resource 的屬性值 String location = ele.getAttribute(RESOURCE_ATTRIBUTE); // 為空,直接退出 if (!StringUtils.hasText(location)) { getReaderContext().error("Resource location must not be empty", ele); return; } // 解析系統屬性,格式如 :"${user.dir}" location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); Set<Resource> actualResources = new LinkedHashSet<>(4); // 判斷 location 是相對路徑還是絕對路徑 boolean absoluteLocation = false; try { absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); } catch (URISyntaxException ex) { // cannot convert to an URI, considering the location relative // unless it is the well-known Spring prefix "classpath*:" } // 絕對路徑 if (absoluteLocation) { try { // 直接根據地址加載相應的配置文件 int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources); if (logger.isDebugEnabled()) { logger.debug("Imported " + importCount + " bean definitions from URL location [" + location + "]"); } } catch (BeanDefinitionStoreException ex) { getReaderContext().error( "Failed to import bean definitions from URL location [" + location + "]", ele, ex); } } else { // 相對路徑則根據相應的地址計算出絕對路徑地址 try { int importCount; Resource relativeResource = getReaderContext().getResource().createRelative(location); if (relativeResource.exists()) { importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource); actualResources.add(relativeResource); } else { String baseLocation = getReaderContext().getResource().getURL().toString(); importCount = getReaderContext().getReader().loadBeanDefinitions( StringUtils.applyRelativePath(baseLocation, location), actualResources); } if (logger.isDebugEnabled()) { logger.debug("Imported " + importCount + " bean definitions from relative location [" + location + "]"); } } catch (IOException ex) { getReaderContext().error("Failed to resolve current resource location", ele, ex); } catch (BeanDefinitionStoreException ex) { getReaderContext().error("Failed to import bean definitions from relative location [" + location + "]", ele, ex); } } // 解析成功后,進行監聽器激活處理 Resource[] actResArray = actualResources.toArray(new Resource[0]); getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele)); }
解析 import 過程較為清晰,整個過程如下:
- 獲取 source 屬性的值,該值表示資源的路徑
- 解析路徑中的系統屬性,如”${user.dir}”
- 判斷資源路徑 location 是絕對路徑還是相對路徑
- 如果是絕對路徑,則調遞歸調用 Bean 的解析過程,進行另一次的解析
- 如果是相對路徑,則先計算出絕對路徑得到 Resource,然后進行解析
- 通知監聽器,完成解析
判斷路徑
方法通過以下方法來判斷 location 是為相對路徑還是絕對路徑:
absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
判斷絕對路徑的規則如下:
- 以 classpath*: 或者 classpath: 開頭為絕對路徑
- 能夠通過該 location 構建出
java.net.URL
為絕對路徑 - 根據 location 構造
java.net.URI
判斷調用isAbsolute()
判斷是否為絕對路徑
如果 location 為絕對路徑則調用 loadBeanDefinitions()
,該方法在 AbstractBeanDefinitionReader 中定義。
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException { 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[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); 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; } }
整個邏輯比較簡單,首先獲取 ResourceLoader,然后根據不同的 ResourceLoader 執行不同的邏輯,主要是可能存在多個 Resource,但是最終都會回歸到 XmlBeanDefinitionReader.loadBeanDefinitions()
,所以這是一個遞歸的過程。
至此,import 標簽解析完畢,整個過程比較清晰明了:獲取 source 屬性值,得到正確的資源路徑,然后調用 loadBeanDefinitions()
方法進行遞歸的 BeanDefinition 加載。