本篇筆記主要記錄了以下內容:
使用 ClassPathXmlApplicationContext,通過在 xml 註冊一個 bean,跟蹤代碼,瞭解它從配置文件的 <bean> 標籤,加載到 BeanFactory 註冊表 beanDefinitionMap 的詳細過程。/<bean>
layout: post- ClassPathXmlApplicationContext - 設置配置文件路徑 - Profile - PropertySource 接口
- Bean 的解析和註冊具體校驗的方法獲取 bean 容器BanFactory 自定義EntityResolver配置文件加載默認標籤解析獲取 id 和 name對標籤中其它屬性的解析BeanDefinitionHolder 修飾prepareBeanFactoryinvokeBeanFactoryPostProcessorsinitMessageSourceonRefreshfinishBeanFactoryInitializationresetCommonCaches
- 踩坑記錄Javadoc 編譯錯誤
- 參考資料
展示的代碼摘取了一些核心方法,去掉一些默認設置和日誌輸出,還有大多數錯誤異常也去掉了,小夥伴想看詳細代碼,註釋和 demo,可以下載我上傳的筆記項目
碼雲 Gitee 地址
Github 地址
通過閱讀源碼的過程,瞭解設計者的設計思路和從中學習,對 spring 有個基礎的瞭解。
ClassPathXmlApplicationContext
ClassPathXmlApplicationContext 的繼承體系結構圖:
這種結構圖是通過 IDEA 編輯器的 Diagrams 功能展示的,對當前類右鍵選擇,可以看到繼承體系,繼承了哪些類和引用了哪些接口,方便我們去了解~
ClassPathXmlApplicationContext 繼承自 AbstractApplicationContext,而 AbstractRefreshableApplicationContext 是 AbstractApplicationContext 的抽象子類,使用的類註冊工廠是 DefaultListableBeanFactory,這個註冊工廠也很重要,後面會有它的介紹。
簡單來說,DefaultListableBeanFactory 是 Spring 註冊及加載 bean 的默認實現,它會將註冊的 bean放入 beanDefinitionMap 進行 key-value 形式存儲。
在圖片的右上角能看到,ResourceLoader 是它的頂層接口,表示這個類實現了資源加載功能。
構造器的代碼:
<code>public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {
super(parent);
// 註釋 1.1 獲取資源文件
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}
/<code>
設置配置文件路徑
org.springframework.context.support.AbstractRefreshableConfigApplicationContext
<code>public void setConfigLocations(@Nullable String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
// 註釋 1.2 將配置資源路徑放入 configLocations 數組中
this.configLocations = new String[locations.length];
for (int i = 0; i < locations.length; i++) {
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
else {
this.configLocations = null;
}
}
/<code>
resolvePath,用途是:解析給定的路徑,用對應的佔位符(placeholder)替換佔位符
例如 new ClassPathXmlApplicationContext("classpath:config.xml");,就需要解析 classpath,變成正確路徑。
<code>protected String resolvePath(String path) {
return getEnvironment().resolveRequiredPlaceholders(path);
}
/<code>
我們有不同的運行環境,dev,test 或者 prod,這個時候加載的配置文件和屬性應該有所不同,這個時候就需要使用到 Environment 來進行區分。
Spring 環境和屬性是由四個部分組成:
- Environment : 環境,由 Profile 和 PropertyResolver 組合。
- Profile : 配置文件,可以理解為,容器裡多個配置組別的屬性和 bean,只有激活的 profile,它對應的組別屬性和 bean 才會被加載
- PropertySource : 屬性源, 使用 CopyOnWriteArrayList 數組進行屬性對 key-value 形式存儲
- PropertyResolver :屬性解析器,這個用途就是解析屬性
Profile
通過這個屬性,可以同時在配置文件中部署兩套配置,用來適用於生產環境和開發環境,這樣可以方便的進行切換開發、部署環境,常用來更換不同的數據庫或者配置文件。
<code>
<beans>
<property-placeholder>
/<beans>
<beans>
<property-placeholder>
/<beans>
<beans>
<property-placeholder>
/<beans>
/<code>
有兩種方式可以設置選擇使用哪套配置:
① 在 web.xml 中設置
<code><context-param>
<param-name>spring.profiles.active/<param-name>
<param-value>test/<param-value>
/<context-param>
/<code>
② 在代碼啟動時設置
<code>context.getEnvironment().setActiveProfiles("test");
/<code>
PropertySource 接口
繼承體系如圖:
從 PropertySource 繼承體系來看,customizePropertySources 方法的調用鏈路是從子類一直往上調用 :
AbstractEnvironment -> StandardServletEnvironment -> StandardEnvironment
最終在 StandardEnvironment 使用 CopyOnWriteArrayList 數組進行屬性存儲
<code>protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
/<code>
例如從上面可以看出,propertySourceList 將會存儲系統的參數:
到時這些參數就能在啟動的應用中,通過上下文 context 進行獲取
<code>((MutablePropertySources)((StandardEnvironment)context.environment).propertySources).propertySourceList
/<code>
Bean 的解析和註冊
Spring bean 的解析和註冊有一個重要的方法 refresh()
AbstractApplicationContext.refresh()
<code>public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing. (為更新準備上下文,設定一些標誌)
prepareRefresh();
// Tell the subclass to refresh the internal bean factory. (告訴子類去更新它們的 bean factory)
// 類的註冊到 bean factory 也是在這一步
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();
}
}
}
/<code>
下面會圍繞這個方法進行跟蹤和分析。
具體校驗的方法
org.springframework.core.env.AbstractPropertyResolver#validateRequiredProperties
<code>public void validateRequiredProperties() {
MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException();
for (String key : this.requiredProperties) {
if (this.getProperty(key) == null) {
ex.addMissingRequiredProperty(key);
}
}
if (!ex.getMissingRequiredProperties().isEmpty()) {
throw ex;
}
}
/<code>
可以看到,校驗邏輯是遍歷 requiredProperties,它是一個字符 Set,默認情況下是空,表示不需要校驗任何元素,如果列表中有值,然後根據 key 獲取對應的環境變量為空,將會拋出異常,導致 Spring 容器初始化失敗。
獲取 bean 容器
在這行代碼中 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
具體調用的是 :
org.springframework.context.support.AbstractRefreshableApplicationContext#refreshBeanFactory
<code>protected final void refreshBeanFactory() throws BeansException {
// 在更新時,如果發現已經存在,將會把之前的 bean 清理掉,並且關閉老 bean 容器
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
// 註釋 1.3 開始加載 (bean 註冊)
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
/<code>
這個入口方法很重要,在這一步新建了 bean 容器和解析 bean,並將 bean 註冊到容器中。
BanFactory 自定義
具體方法如下,通過這個方法,可以對工廠進行定製化設置,讓子類進行自由配置:
org.springframework.context.support.AbstractRefreshableApplicationContext#customizeBeanFactory
<code>protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
if (this.allowBeanDefinitionOverriding != null) {
// 默認是 false,不允許覆蓋
beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.allowCircularReferences != null) {
// 默認是 false,不允許循環引用
beanFactory.setAllowCircularReferences(this.allowCircularReferences);
}
}
/<code>
EntityResolver
接口全路徑是:org.xml.sax.EntityResolver,具體解析使用的方法是:
org.springframework.beans.factory.xml.ResourceEntityResolver#resolveEntity
該方法是用於解析 schema 和 dtd,具體深究的話也很複雜,但解析 xml 不是我想了解的點,所以先跳過~
配置文件加載
入口方法:(由於有多個重名方法,所以複製路徑時,將參數的類型也拷貝了)
org.springframework.beans.factory.support.AbstractBeanDefinitionReader#loadBeanDefinitions(java.lang.String, java.util.Set<org.springframework.core.io.resource>)/<org.springframework.core.io.resource>
核心方法是這兩行
<code>public int loadBeanDefinitions(String location, @Nullable Set<resource> actualResources) throws BeanDefinitionStoreException {
// 獲取資源文件(資源加載器從路徑識別資源文件)
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location)
// 註釋 1.6 根據資源文件加載 bean
int count = loadBeanDefinitions(resources);
···
}
/<resource>/<code>
獲取資源文件後,開始解析資源文件(也就是一開始傳參的 config.xml),將它轉換成 Document
跟蹤代碼可以看到,進行解析的資源文件從 Resource 包裝成 EncodeResouce,為輸入流添加了字符編碼(默認為 null),體現了設計模式
- 裝飾器模式
遍歷資源文件,進行轉換,核心方法是以下兩行:
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#loadBeanDefinitions(org.springframework.core.io.support.EncodedResource)
<code>public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
// 註釋 1.7 從資源文件中獲取輸入流
InputStream inputStream = encodedResource.getResource().getInputStream();
InputSource inputSource = new InputSource(inputStream);
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
/<code>
默認標籤解析
這部分不會細說,之後再寫一篇進行補充,所以簡單的過下代碼中,是如何解析默認標籤的
- IMPORT:導入標籤
- ALIAS:別名標籤
- BEAN:bean 標籤
- NESTED_BEANS:beans 標籤(嵌套的 beans)
org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#parseDefaultElement
<code>private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
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)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}
/<code>
讓我們來看下如何解析 bean 標籤
獲取 id 和 name
org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseBeanDefinitionElement(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinition)
<code>public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
// 獲取 ID 屬性
String id = ele.getAttribute(ID_ATTRIBUTE);
// 獲取 NAME 屬性
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));
}
String beanName = id;
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
// 如果沒有指定 id,將 name 的第一個值作為 id
beanName = aliases.remove(0);
}
// 默認 null
if (containingBean == null) {
// 檢查名字是否唯一,如果 id 重複了,將拋出錯誤
// 內部 usedNames 是一個 HashSet,將會存儲加載過的 name 和 aliases
checkNameUniqueness(beanName, aliases, ele);
}
// 將公共屬性放入 AbstractBeanDefinition,具體實現在子類 GenericBeanDefinition
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
if (!StringUtils.hasText(beanName)) {
if (containingBean != null) {
// 如果 id 和 name 都是空,那個 spring 會給它生成一個默認的名稱
beanName = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, this.readerContext.getRegistry(), true);
}
else {
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);
}
}
}
}
String[] aliasesArray = StringUtils.toStringArray(aliases);
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}
return null;
}
/<string>/<code>
獲取 id 和 name 屬性的流程,按照代碼註釋一步一步往下走就清晰了
該方法主要工作流程如下:
- 提取元素中的 id name 屬性
- 進一步解析其它所有屬性並統一封裝到 GenericBeanDefinition 類型的實例中
- 檢測到 bean 沒有指定 beanName 使用默認規則生成 beanName
- 將獲取到的信息封裝到 BeanDefinitionHolder 的實例中
對標籤中其它屬性的解析
org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseBeanDefinitionElement(org.w3c.dom.Element, java.lang.String, org.springframework.beans.factory.config.BeanDefinition)
<code>public static AbstractBeanDefinition createBeanDefinition(
@Nullable String parentName, @Nullable String className, @Nullable ClassLoader classLoader) throws ClassNotFoundException {
GenericBeanDefinition bd = new GenericBeanDefinition();
bd.setParentName(parentName);
if (className != null) {
if (classLoader != null) {
bd.setBeanClass(ClassUtils.forName(className, classLoader));
}
else {
bd.setBeanClassName(className);
}
}
return bd;
}
/<code>
初始化 BeanDefiniton 在這個方法中:(具體實現是它的子類 GenericBeanDefinition 噢~)
BeanDefinitionReaderUtils.createBeanDefinition(parentName, className, this.readerContext.getBeanClassLoader())
<code>public BeanDefinitionHolder decorateBeanDefinitionIfRequired(
Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) {
// 方法中的第三個參數是父類 bean
// 當對某個嵌套配置進行分析時,這裡需要傳遞,是為了使用父類的 scope 屬性,以備子類沒設定 scope,可以使用父類的 scope 屬性
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;
}
/<code>
後面就是解析其它標籤的內容,之後會補坑~
BeanDefinitionHolder 修飾
org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#decorateBeanDefinitionIfRequired(org.w3c.dom.Element, org.springframework.beans.factory.config.BeanDefinitionHolder, org.springframework.beans.factory.config.BeanDefinition)
<code>public BeanDefinitionHolder decorateBeanDefinitionIfRequired(
Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) {
// 方法中的第三個參數是父類 bean
// 當對某個嵌套配置進行分析時,這裡需要傳遞,是為了使用父類的 scope 屬性,以備子類沒設定 scope,可以使用父類的 scope 屬性
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;
}
/<code>
在之前的常規屬性解析後,在這一步操作中,主要用來完成自定義標籤元素的解析,這裡繼續留個坑~
prepareBeanFactory
準備類加載器的環境,對前面獲取到的 beanFactory(ConfigurationListableBeanFactory) 進行相關的設置,包括 ClassLoader, post-processors等
invokeBeanFactoryPostProcessors
實例化並調用所有註冊的 BeanFactoryPostProcessorBean,這些是後處理器,處理類型是 BeanFactory, Spring 容器允許在實例化 bean 前,讀取 bean 信息和修改它的屬性。
相當於在實例化前,給用戶最後一次機會去修改 bean 信息。
還有一點,執行也可以有先後順序,依據這些處理器是否實現 PriorityOrdered 、Order 接口,根據 order 值進行排序。
initMessageSource
初始化此上下文的消息源
onRefresh
模板方法,可被重寫以添加特定於上下文的刷新工作。
在實例化單例之前調用特殊 bean 的初始化。(霧,不知道是啥特殊 bean ,留個坑=-=)
此實現為空。
finishBeanFactoryInitialization
完成 bean 容器的初始化,實例化所有剩餘的(非惰性初始化)單例
resetCommonCaches
真真註冊的最後一步,用來清除緩存
重置 Spring 核心中的公共內省緩存,因為我們可能再也不需要單例 bean 的元數據了
踩坑記錄
Javadoc 編譯錯誤
Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting)
在編譯時,發現無法成功,提示 Javadoc 的錯誤,解決方法是在 gradle 文件中添加以下配置:
<code>tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
options.addStringOption('encoding', 'UTF-8')
}/<code>
閱讀更多 科技伍小黑 的文章