Spring載入bean定義流程原始碼解析
在本文中,先對Spring載入和建立bean例項的流程跟著原始碼走一遍,在後續的文章中再對所涉及的類的其他方法具體介紹。
//這一步是載入指定的配置檔案 Resource resource = new ClassPathResource("bean.xml"); //DefaultListableBeanFactory是BeanFactory的子類中最常用的一個類,例項化它作為我們IOC的容器 DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); //這個類是用來解析配置檔案的,將配置檔案中配置的bean載入到容器中 XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); //載入bean reader.loadBeanDefinitions(resource); //這一步是從容器中獲取指定的bean,這一步也牽扯到了bean的例項化 Person person = (Person) beanFactory.getBean("person");
下面,我們對上面的每一步都進行分析:
Resource resource = new ClassPathResource("bean.xml");
這一步是將我們的配置檔案的路徑,名稱等屬性放到一個類中,以便我們很輕鬆的獲得它的各種資訊。
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
例項化了一個工廠類,作為IOC容器,有關它的其它方法,在後續的文章中還會介紹
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
例項化了一個xml閱讀器,這個類會將配置檔案中所有的bean載入到beanFactory中。
reader.loadBeanDefinitions(resource);
閱讀器開始載入bean,從這開始,我們跟進原始碼去探索他是怎麼實現的:
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
進去之後發現,它又呼叫了自身的方法,只不過將我們傳進去的resource進行了包裝,變成了EncodedResource,這只不過是記錄了resource本身,以及它的字符集和編碼資訊。 繼續:
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
//斷言我們傳入的encodedResource不是空的
Assert.notNull(encodedResource, "EncodedResource must not be null");
//列印日誌
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
//這是一個放在當前執行緒中的set
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
//Spring剛開始啟動的時候肯定是空的,我們要建立一個set,並放到當前執行緒中
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 {
//得到配置檔案的輸入流
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
//對輸入流進行封裝
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
//從這開始,才真正的進行bean的載入
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();
}
}
}
我們發現,這個方法中邏輯也很簡單,就是先從當前執行緒中獲取到我們記錄我們已經正在載入的配置檔案的set集合,如果沒有則建立,之後獲取配置檔案的輸入流包裝成inputSource和resource一起傳到下一個方法中。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
...
//得到配置檔案的document物件
Document doc = doLoadDocument(inputSource, resource);
//註冊bean
return registerBeanDefinitions(doc, resource);
...
}
這個方法中一個就做了兩件事,但是這兩件事都不簡單,每一個邏輯都夠我們研究很長時間的,,先是得到配置檔案的document物件,有了它,我們就可以對它的們一個節點進行解析了,之後,就是要將bean註冊到beanFactory中了。 第一步就是對xml物件的建立,我們主要來看第二步:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
//建立一個載入bean的解析器
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//得到這個工廠中之前已經註冊的bean的個數
int countBefore = getRegistry().getBeanDefinitionCount();
//開始註冊bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//返回本次註冊bean的個數
return getRegistry().getBeanDefinitionCount() - countBefore;
}
其實到這我們也能發現,Spring把很多複雜的邏輯都給拆分成了一個個小的部分,顯得很有條理,也便於我們閱讀。我們繼續跟進bean的註冊:
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
//獲得document的根節點
Element root = doc.getDocumentElement();
//到這才真正開始註冊
doRegisterBeanDefinitions(root);
}
這個方法也並沒有做什麼,我們發現Spring中真正執行邏輯的往往都是do開頭的方法:
protected void doRegisterBeanDefinitions(Element root) {
//在這會產生一個委託,由於在配置檔案中可能會有巢狀的配置檔案,所以會存在遞迴呼叫
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
...
//前置處理
preProcessXml(root);
//解析bean
parseBeanDefinitions(root, this.delegate);
//後置處理
postProcessXml(root);
this.delegate = parent;
}
在這個方法中提出了委託,一個delegate就代表了一個beans,是Spring配置檔案中的根標籤,在後面的解析中,也都是委託給他來做的,createDelegate()這個方法中就是對beans這個標籤的解析。我們發現在這個方法中有兩個方法叫做:preProcessXml和postProcessXml,點選去發現他們是空的,其實這交給使用者定製去實現一些特殊需求的。繼續跟進程式碼:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
//當前所要解析的配置檔案是不是預設的名稱空間
if (delegate.isDefaultNamespace(root)) {
//獲得所有的孩子節點,一個孩子節點就是一個bean
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
//當前孩子節點是否是一個element
if (node instanceof Element) {
Element ele = (Element) node;
//是否是預設的名稱空間
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
//單獨處理
delegate.parseCustomElement(ele);
}
}
}
}
else {
//單獨處理
delegate.parseCustomElement(root);
}
}
這個方法主要是對節點是否是預設的名稱空間,分別進行解析,在這裡,我們只要看一下對預設名稱空間的解析:
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
//對import標籤解析
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
//對alias標籤解析
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
//對bean標籤解析
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
這個方法是對不同的標籤提供了不同的解析方法,我們先對bean標籤的解析進行研究,以後還會對其他標籤進行研究
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
//解析當前節點
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
//解析完節點後還需要對他進行屬性填充等工作
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// 註冊bean
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// 產生一個時間
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
撥開雲霧見天日,終於開始解析了:
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
//首先是要獲取當前bean的id和name屬性
String id = ele.getAttribute(ID_ATTRIBUTE);
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
//獲取別名
List<String> aliases = new ArrayList<>();
if (StringUtils.hasLength(nameAttr)) {
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
aliases.addAll(Arrays.asList(nameArr));
}
將bean的名字設定為id
String beanName = id;
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
beanName = aliases.remove(0);
if (logger.isDebugEnabled()) {
logger.debug("No XML 'id' specified - using '" + beanName +
"' as bean name and " + aliases + " as aliases");
}
}
if (containingBean == null) {
checkNameUniqueness(beanName, aliases, ele);
}
//解析bean中的其他屬性
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
//如果當前bean定義中的beanName為空,那麼就要為當前bean按照指定規則生成一個名字
if (!StringUtils.hasText(beanName)) {
try {
if (containingBean != null) {
beanName = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, this.readerContext.getRegistry(), true);
}
else {
beanName = this.readerContext.generateBeanName(beanDefinition);
// Register an alias for the plain bean class name, if still possible,
// if the generator returned the class name plus a suffix.
// This is expected for Spring 1.2/2.0 backwards compatibility.
String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName != null &&
beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
aliases.add(beanClassName);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Neither XML 'id' nor 'name' specified - " +
"using generated bean name [" + beanName + "]");
}
}
catch (Exception ex) {
error(ex.getMessage(), ele);
return null;
}
}
//將別名變成字串陣列
String[] aliasesArray = StringUtils.toStringArray(aliases);
//設定完引數返回
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}
return null;
}
這個方法中主要是對bean標籤裡面的各種屬性進行解析,在配置檔案中沒有宣告的就使用預設值,還沒有進行對bean標籤子節點的解析,在這裡我們看見出現了一個新的類BeanDefinitionHolder,它其實就是對beanDefinition,beanName和aliasesArray的暫時封裝,等到當前bean全部解析完之後會將他所封裝的資訊放大IOC容器中。
public BeanDefinitionHolder decorateBeanDefinitionIfRequired(
Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) {
BeanDefinitionHolder finalDefinition = definitionHolder;
//有些屬性沒有直接放到bean標籤裡面,而是放到了bean標籤的下一級中來宣告,對這些屬性進行解析
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;
}
在這個方法中主要是對bean標籤裡面那些不是預設名稱空間的節點進行解析,並把值設定到bean定義裡面。到這為止,一個完整的bean就被解析完了,下面就要把解析完的bean註冊到IOC容器當中去:
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {
// 得到bean的名字
String beanName = definitionHolder.getBeanName();
//根據beanName註冊bean
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
// 註冊別名
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
//將別名一個一個的註冊
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}
我們分開來看,先看一下注冊bean的過程:
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
...
//看一下這個bean是不是被註冊過
BeanDefinition oldBeanDefinition;
oldBeanDefinition = this.beanDefinitionMap.get(beanName);
if (oldBeanDefinition != null) {
if (!isAllowBeanDefinitionOverriding()) {
//這個bean已經被註冊過了,並且不允許被覆蓋,丟擲異常
...
}
...
//覆蓋註冊
this.beanDefinitionMap.put(beanName, beanDefinition);
}
else {
//此時有其他bean開始建立,為了防止併發需要對當前IOC容器上鎖,如果此時沒有其他的bean正在建立,直接進行註冊
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
//將bean註冊到容器中
this.beanDefinitionMap.put(beanName, beanDefinition);
//更新已經註冊的bean的名字的列表
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 {
// Still in startup registration phase
this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);
this.manualSingletonNames.remove(beanName);
}
this.frozenBeanDefinitionNames = null;
}
if (oldBeanDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
}
這個方法首先檢查有沒有相同的beanName被註冊並作出相應的處理,然後將beanName作為key,beanDefinition作為value放到IOC容器中,並且更新已經註冊的bean的名字列表。當有其他bean正在建立的時候,為了防止併發,對IOC容器進行了上鎖。
public void registerAlias(String name, String alias) {
...
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 + "'.");
}
}
//檢查是否存在別名的迴圈。如果存在,丟擲異常
checkForAliasCircle(name, alias);
//將別名新增到別名列表中
this.aliasMap.put(alias, name);
}
}
好了,到這一個bean的載入就完成了,讓我們來回顧一下: 一、指定要載入的配置檔案 二、例項化一個IOC容器 三、例項化一個解析配置檔案的閱讀器,並設定好我們例項化的IOC容器 四、開始載入bean 1、封裝resource,進入下一個方法進行處理 2、記錄當前正在進行載入的resource 3、獲得配置檔案的輸入流,開始解析 4、將xml檔案裝換成document物件,對每個節點解析 5、建立一個解析document物件的解析器,獲得root節點,開始解析 6、正式解析之前和解析之後分別有一個前置處理和後置處理,交給使用者對於特殊需求的自定義實現 7、遍歷root的所有孩子節點,根據節點是否是預設的名稱空間,選擇不同的解析方法 8、根據不同的標籤選擇不同的解析方法,在本文中是以bean標籤舉例的 9、將bean標籤的所有屬性封裝到beanDefinition中,並且將beanDefinition、beanName以及alias封裝到beanDefinitionHolder中做進一步處理 10、解析完成,將別名、beanDefinition根據beanName註冊到IOC容器中,等待例項建立 在下一篇文章中,我們將對Spring對bean例項的建立流程進行原始碼的解讀。