讀完這篇文章你將會收穫到
- 瞭解到 Spring 容器初始化流程
- ThreadLocal 在 Spring 中的最佳實踐
- 面試中回答 Spring 容器初始化流程
引言
我們先從一個簡單常見的代碼入手分析
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<bean class="com.demo.data.Person">
<description>
微信搜一搜:CoderLi(不妨關注➕一下?這次一定?)
</description>
</bean>
</beans>
public static void main(String[] args) {
Resource resource = new ClassPathResource("coderLi.xml");
DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
xmlBeanDefinitionReader.loadBeanDefinitions(resource);
}
上面這段 Java 代碼主要做了
- 資源的獲取(定位)
- 創建一個 beanFactory
- 根據 beanFactory (實現了 BeanDefinitionRegistry 接口) 創建一個 beanDefinitionReader
- 裝載資源並 registry 資源裏面的 beanDefinition
所以總體而言就是資源的加載、加載、註冊三個步驟
-
對於資源的加載可以看看我另一篇文章 Spring-資源加載(源碼分析)
-
加載的過程則是將 Resource 對象轉爲一系列的 BeanDefinition 對象
-
註冊則是將 BeanDefinition 注入到 BeanDefinitionRegistry 中
組件介紹
在分析源碼流程之前我們一起先對一些重要的組件混個眼熟
DefaultListableBeanFactory
defaultListableBeanFactory 是整個 bean 加載的核心部分,是 bean 註冊及加載 bean 的默認實現
對於 AliasRegistry 可以參考我另一篇文章 Spring-AliasRegistry 。關於這個類我們只要記住兩點,一個是它是一個 beanFactory、一個是它是一個 BeanDefinitionRegistry
XmlBeanDefinitionReader
從 XML 資源文件中讀取並轉換爲 BeanDefinition 的各個功能
DocumentLoader
對 Resource 文件進行轉換、將 Resource 文件轉換爲 Document 文件
BeanDefinitionDocumentReader
讀取 Document 並向 BeanDefinitionRegistry 註冊
源碼分析
loadBeanDefinitions(Resource)
XmlBeanDefinitionReader#loadBeanDefinitions(Resource)
我們先從這個入口方法開始進去
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
EncodedResource
是 Resource 的子類, Spring-資源加載(源碼分析)
public class EncodedResource implements InputStreamSource {
private final Resource resource;
@Nullable
private final String encoding;
@Nullable
private final Charset charset;
..........
..........
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());
}
}
}
只是一個簡單的 Wrapper 類,針對不同的字符集和字符編碼返回不一樣的 Reader
loadBeanDefinitions(EncodedResource)
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
// 從Thread Local 中獲取正在加載的的資源
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
// 判斷這個資源是否已經加載過了、主要是爲了是否是 資源的循環依賴 import
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException("");
}
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
InputSource inputSource = new InputSource(inputStream);
// 有encode 就設置進去
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 真正的加載
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("");
}
finally {
// ThreadLocal的最佳實踐
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
首先從 ThreadLocal
中獲取正在加載的 Resource
,這裏主要是檢查 import
標籤帶來的循環引用問題。
從這裏我們可以看到在 finally
中,對已經完成加載的資源進行移除,並且檢查 Set
是否還有元素了,如果沒有則直接調用 ThreadLocal
的 remove
方法。這個就是 ThreadLocal 的最佳實踐了,最後的 remove
方法的調用可以避免 ThreadLocal 在 ThreadLocalMap 中作爲 WeakReference
而帶來的內存泄露問題。
這個方法裏基本做啥事情、最主要的事情就是調用了 doLoadBeanDefinitions
這個方法,而這個方法纔是真正幹活的。(在 Spring 中,很有意思的是、真正幹活的方法前綴都是帶有 do
的,這個可以留意下)
doLoadBeanDefinitions(InputSource Resource)
// 獲取 document 對象
Document doc = doLoadDocument(inputSource, resource);
// 註冊 bean definition
int count = registerBeanDefinitions(doc, resource);
return count;
doLoadDocument
這個方法就是將 Resource 轉化爲 Document,這裏涉及到 xml 文件到驗證,建立對應的 Document Node ,使用到的就是上面提及到的 DocumentLoader
。這個不展開來探討。
我們直接進入到 registerBeanDefinitions
方法中
registerBeanDefinitions(Document,Resource)
public int registerBeanDefinitions(Document doc, Resource resource) {
// 創建一個 bean definition 的 reader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 註冊之前已經有的 bean definition 的個數 return this.beanDefinitionMap.size();
int countBefore = getRegistry().getBeanDefinitionCount();
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
上面代碼中出現了一個我們提及到的 BeanDefinitionDocumentReader
組件,他的功能就是讀取 Document 並向 BeanDefinitionRegistry 註冊
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}
這裏又來了、do
纔是真正幹活的大哥
protected void doRegisterBeanDefinitions(Element root) {
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
// 處理 profiles
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
return;
}
}
}
// 解釋前的處理 這裏默認爲空實現、子類可以覆蓋此方法在解釋 Element 之前做些事情
preProcessXml(root);
// 解釋
parseBeanDefinitions(root, this.delegate);
// 解釋後處理 這裏默認爲空實現
postProcessXml(root);
this.delegate = parent;
}
這裏主要的方法就是 parseBeanDefinitions
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// spring 默認標籤解釋
parseDefaultElement(ele, delegate);
} else {
// 自定義 標籤解釋
delegate.parseCustomElement(ele);
}
}
}
} else {
delegate.parseCustomElement(root);
}
}
Spring 的默認標籤有 import
, beans
, bean
, alias
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
// 解釋 import 標籤
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
} else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
} else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
} else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
doRegisterBeanDefinitions(ele);
}
}
解釋 import
標籤調用 importBeanDefinitionResource
最終會調用到我們最開始處理 Resource
循環依賴的那個方法 loadBeanDefinitions
中
我們直接進入到 processAliasRegistration
方法中
protected void processAliasRegistration(Element ele) {
String name = ele.getAttribute(NAME_ATTRIBUTE);
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 {
// 最重要的一行代碼
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));
}
}
最重要的一行代碼就是將 name 和 alias 進行註冊(這裏註冊的是 alias 標籤中的 name 和 alias 之間的關係),可以參考這篇文章進行了解 Spring-AliasRegistry
我們來到最主要的 processBeanDefinition
protected void processBeanDefinition(Element ele,
BeanDefinitionParserDelegate delegate) {
// 這裏獲得了一個 BeanDefinitionHolder
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
} catch (BeanDefinitionStoreException ex) {
.....
}
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
我們先分析 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())
這句代碼
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {
// 註冊 bean Name
String beanName = definitionHolder.getBeanName();
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
// 註冊 alias .
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}
這個方法的作用很簡單、就是使用一開始我們傳給 XmlBeanDefinitionReader
的 BeanDefinitionRegistry
對 bean 和 beanDefinition 的關係進行註冊。並且也對 beanName 和 alias 的關係進行註冊(這裏是對 bean 標籤中配置的 id 和 name 屬性關係進行配置)
delegate.parseBeanDefinitionElement(ele)
我們再把眼光返回到這個方法、這個方法就是創建 BeanDefinition 的地方了
@Nullable
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {
return parseBeanDefinitionElement(ele, null);
}
@Nullable
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
String id = ele.getAttribute(ID_ATTRIBUTE);
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
List<String> aliases = new ArrayList<>();
// 判斷是否配置了 name 屬性、對name 進行分割
// 在 bean 標籤中 name 就是 alias 了
if (StringUtils.hasLength(nameAttr)) {
String[] nameArr = StringUtils.tokenizeToStringArray(...);
aliases.addAll(Arrays.asList(nameArr));
}
String beanName = id;
// 沒有配置id 並且 alias 列表不爲空、則選取第一個 alias 爲 bean Name
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
beanName = aliases.remove(0);
}
if (containingBean == null) {
// 檢查 beanName 和alias 的唯一性
checkNameUniqueness(beanName, aliases, ele);
}
// 怎麼生成一個BeanDefinition 尼
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
// 如果 beanName 爲 空
if (!StringUtils.hasText(beanName)) {
try {
if (containingBean != null) {
beanName = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, this.readerContext.getRegistry(), true);
} else {
// 沒有配置 beanName 和 alias的話、那麼這個類的第一個實例、將擁有 全類名的alias
// org.springframework.beans.testfixture.beans.TestBean 這個是別名(TestBean#0 才擁有這個別名、其他的不配擁有)
// org.springframework.beans.testfixture.beans.TestBean#0 這個是 beanName
beanName = this.readerContext.generateBeanName(beanDefinition);
String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName != null &&
beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
aliases.add(beanClassName);
}
}
} catch (Exception ex) {
.........
}
}
String[] aliasesArray = StringUtils.toStringArray(aliases);
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}
// nothing
return null;
}
在 bean 標籤中 name 屬性對應的就是 alias ,id 屬性對應的就是 beanName 了
當我們沒有配置 id 屬性但是配置了 name 屬性、那麼第一個 name 屬性就會成爲我們的 id
當我們既沒有配置 id 屬性 也沒有配置 name 屬性,那麼 Spring 就會幫我們生成具體可看看 Spring-AliasRegistry
然後就創建了一個 BeanDefinitionHolder 返回了
上面的代碼我們看到有這個關鍵的方法 parseBeanDefinitionElement(ele, beanName, containingBean)
這個方法生成了我們期待的 BeanDefinition ,但是裏面的內容都是比較枯燥的
// 解釋class 屬性
String className = null;
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
}
// 是否指定了 parent bean
String parent = null;
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
parent = ele.getAttribute(PARENT_ATTRIBUTE);
}
// 創建 GenericBeanDefinition
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
// 解釋各種默認的屬性
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
// 提取describe
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
// 解釋元數據
parseMetaElements(ele, bd);
// look up 方法
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
// replacer
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
// 解析構造函數參數
parseConstructorArgElements(ele, bd);
// 解釋property子元素
parsePropertyElements(ele, bd);
// 解釋qualifier
parseQualifierElements(ele, bd);
bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));
都是去解析 bean 標籤裏面的各種屬性
那麼我們整個 Spring 容器初始化流程就介紹完了
總結
public static void main(String[] args) {
Resource resource = new ClassPathResource("coderLi.xml");
DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
xmlBeanDefinitionReader.loadBeanDefinitions(resource);
}
- 調用 XmlBeanDefinitionReader 的方法 loadBeanDefinitions
- 將 Resource 包裹成 EncodeResource
- 通過 ThreadLocal 判斷是否 Resource 循環依賴
- 使用 DocumentLoader 將 Resource 轉換爲 Document
- 使用 BeanDefinitionDocumentReader 解釋 Document 的標籤
- 解釋 Spring 提供的默認標籤/自定義的標籤解釋
- 解釋 import 標籤的時候會回調到步驟2中
- 解釋 alias 標籤會向 AliasRegistry 註冊
- 解釋 bean 標籤會向 BeanDefinitionRegistry 註冊 beanName 和 BeanDefinition ,也會註冊 bean 標籤裏面 id 和 name 的關係(其實就是 alias )
大致的流程就是如此了,面試的時候大致說出 XmlBeanDefinitionReader,DocumentLoader,BeanDefinitionDocumentReader,BeanDefinitionRegistry,AliasRegistry
這幾個組件,面試官大概率會認爲你是真的看過 Spring 的這部分代碼的
往期相關文章