1、概述
spring的兩大核心:IOC(依賴注入)和AOP(面向切面),IOC本質上就是一個執行緒安全的hashMap,put和get方法就對應IOC容器的bean的註冊和獲取,spring通過讀取xml或者使用註解配置的類生成一個BeanDefinition放入到容器中,獲取的時候通過BeanDefinition的配置通過asm、反射等技術完成屬性的注入最終獲取一個bean,獲取bean的方法就getBean(),我們無需關心實現細節,直接按照spring提供的註解或者xml配置方式使用即可。
2、IOC容器
雖然IOC本質上是一個執行緒安全的hashMap,使用時直接通過getBean()獲取(@Autowired本質也是通過getBean()獲取),這樣在使用bean例項的時候,就不用關心bean的建立,只管用就行了,IOC會在程式啟動時,自動將依賴的物件注入到目標物件中,非常簡單,省心。但是如果不瞭解IOC中bean的註冊和獲取原理,當使用Spring無法獲取一個bean的時候,針對丟擲的異常可能一頭霧水。
IOC容器的實現包含了兩個非常重要的過程:
- xml的讀取生成BeanDefinition註冊到IOC中
- 通過BeanDefinition例項化bean,並從IOC中通過getBean()獲取
//spring原始碼中將一個bean的BeanDefinition放入到IOC中
this.beanDefinitionMap.put(beanName, beanDefinition);
複製程式碼
//Spring原始碼中通過beanName獲取一個bean
public Object getBean(String name) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name);
}
複製程式碼
這兩個過程還對應了兩個非常核心的介面:
BeanDefinitionRegistry和BeanFactory,一個向IOC中註冊BeanDefinition,一個從IOC獲取Bean例項物件。
讀IOC原始碼必須從瞭解BeanDefinition開始,BeanDefinition是一個介面,無論是通過xml宣告還是通過註解定義一個bean例項,在IOC容器中第一步總是為其對應生成一個BeanDefinition,裡面包含了類的所有基本資訊。
其中AbstractBeanDefinition實現BeanDefinition,這個介面有兩個子介面GenericBeanDefinition和RootBeanDefinition,再來看看BeanDefinition中的一些方法
-
getBeanClassName()獲取bean的全限定名,
-
getScope()獲取該類的作用域,
-
isLazyInit()該類是否為懶載入,
-
getPropertyValues()獲取配置的屬性值列表(用於setter注入),
-
getConstructorArgumentValues()獲取配置的建構函式值(用於構造器注入)等bean的重要資訊,如果通過註解的方式,還會包含一些註解的屬性資訊,
總而言之 ,BeanDefinition包含了我們定義的一個類的所有資訊,然後通過 BeanDefinitionRegistry介面的registerBeanDefinition註冊到IOC容器中,最後通過BeanDefinition結合asm,反射等相關技術,通過BeanFactory介面的getBean()獲取一個例項物件好像也不是什麼困難的事情了。當然這只是表層的大致原理,實際上spring在實現IOC的時候,用了大量的設計模式,比如:單例模式、模板方法、工廠模式、代理模式(AOP基本上全是)等,此外物件導向的基本原則中的單一職責、開放封閉原則等隨處可見,具體的原始碼解讀還是在之後的筆記裡介紹。
3、讀取xml配置檔案
讀取原始碼需要通過除錯去看,Spring啟動時首先會讀取xml配置檔案,xml檔案可以從當前類路徑下讀,也可以從檔案系統下讀取,以下是用於除錯的簡單案例:
@Test
public void testSpringLoad() {
ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("spring/spring-context.xml");
BankPayService bankPayService = (BankPayService) application.getBean("bankPayService");
Assert.assertNotNull(bankPayService);
}
複製程式碼
xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
default-lazy-init="true">
<bean id="bankPayService" class="com.yms.manager.serviceImpl.BankPayServiceImpl"/>
<context:property-placeholder location="classpath*:app-env.properties"/>
<context:component-scan base-package="com.yms.market"/>
</beans>
複製程式碼
執行測試案例,肯定是成功的,打斷點開始除錯,首先會進入ClassPathXmlApplicationContext的建構函式中:
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent){
super(parent);
//獲取當前環境 裝飾傳入的路徑
setConfigLocations(configLocations);
if (refresh) {
//程式入口
refresh();
}
}
複製程式碼
建構函式中最關鍵的部分是refresh()方法,該方法用於重新整理IOC容器資料,該方法由AbstractApplicationContext實現。
@Override
public void refresh() {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
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) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}}}
複製程式碼
在refresh方法中主要完成了載入xml檔案的環境配置、xml檔案讀取,註冊BeanFactoryPostProcessor處理器、註冊監聽器等工作,其中比較核心的是第二行程式碼ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
//載入xml配置檔案,生成BeanDefinition並註冊到IOC容器中
refreshBeanFactory();
//獲取載入完xml檔案之後的beanFactory物件
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}
複製程式碼
refreshBeanFactory和getBeanFactory都是由AbstractApplicationContext的子類AbstractRefreshableApplicationContext實現的,
@Override
protected final void refreshBeanFactory() throws BeansException {
//如果beanFactory不為空 ,清除老的beanFactory
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
//建立一個beanFactory
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
//設定bean是否允許覆蓋 是否允許迴圈依賴
customizeBeanFactory(beanFactory);
//載入beans宣告,即讀取xml或者掃描包 生成BeanDefinition註冊到IOC
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
複製程式碼
在Spring中DefaultListableBeanFactory是一個非常重要的類,它實現了BeanDefinitionRegistry和BeanFactory介面,並且完成了這兩個介面的具體實現,DefaultListableBeanFactory的類圖如下:
我們已經知道BeanDefinitionRegistry完成了BeanDefinition的註冊,BeanFactory完成了getBean()中bean的建立,其中xml讀取和bean的 註冊的入口就是loadBeanDefinitions(beanFactory)這個方法,loadBeanDefinitions是一個抽象方法,由類AbstractXmlApplicationContext實現。loadBeanDefinitions在AbstractXmlApplicationContext有很多個過載方法,在不通階段方法使用的引數值不同,接下來看看各個loadBeanDefinitions的呼叫順序:
建立XmlBeanDefinitionReader物件
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
//Spring將讀取xml操作委託給了XmlBeanDefinitionReader物件
//並且傳入DefaultListableBeanFactory將生成的beandefinition註冊到IOC中
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
//設定Spring中bean的環境
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
// 設定beanDefinitionReader驗證屬性,子類可以重寫該方法使用自定義reader物件
initBeanDefinitionReader(beanDefinitionReader);
//通過beanDefinitionReader讀取xml
loadBeanDefinitions(beanDefinitionReader);
}
複製程式碼
Spring對於讀取xml檔案。並不是由DefaultListableBeanFactory親力親為,而是委託給了XmlBeanDefinitionReader,在該類內部會將xml配置檔案轉換成Resource,Spring封裝了xml檔案獲取方式,我們使用ClassPathXmlApplicationContext讀取xml,因此Spring會通過ClassLoader獲取當前專案工作目錄,並在該目錄下查詢spring-context.xml檔案,當然我們還可以使用FileSystemXmlApplicationContext從檔案系統上以絕對路徑的方式讀取檔案
繼續檢視第二個loadBeanDefinitions(beanDefinitionReader):
獲取配置檔案路徑集合
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
//預設返回為空 子類可以實現該方法 讀取指定檔案
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
//獲取我們配置的xml檔案路徑集合
String[] configLocations = getConfigLocations();
if (configLocations != null)
{
reader.loadBeanDefinitions(configLocations);
}
}
複製程式碼
在這個方法中,最終獲取到了我們通過ClassPathXmlApplicationContext物件傳進來的xml配置檔案路徑,然後由進入委託物件XmlBeanDefinitionReader的loadBeanDefinitions方法中:
@Override
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
Assert.notNull(locations, "Location array must not be null");
int counter = 0;
for (String location : locations) {
//迴圈讀取配置的所有配置檔案路徑
counter += loadBeanDefinitions(location);
}
//返回此次載入的BeanDefinition個數
return counter;
}
複製程式碼
在XmlBeanDefinitionReader中,會迴圈讀取配置的所有配置檔案路徑,並將讀取到的bean的宣告建立成BeanDefinition,並將此次生成的數量返回,繼續檢視loadBeanDefinitions:
public int loadBeanDefinitions(String location, 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;
}
}
複製程式碼
這個方法就是上面所說的Spring將xml配置檔案封裝成Resourse,最終獲取到Resourse的過程,這部分程式碼沒什麼好看的,就是找到ClassPathResource將xml路徑放進去,然後呼叫loadBeanDefinitions(resources),再來看這個方法:
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
複製程式碼
這個方法將Resource封裝城了EncodedResource物件,這個物件有一個屬性encoding,如果設定了xml檔案的編碼,在這裡讀取xml檔案的時候會根據該編碼進行讀取,繼續往下看:
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
//獲取前面載入到的xml配置資原始檔
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 {
//之所以將路徑都封裝到Resource裡面,就是使其提供一個統一的getInputStream方法
//獲取檔案流物件,XmlBeanDefinitionReader無需關心xml檔案怎麼來的
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
//終於來到了最最核心的方法 解析檔案流
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();
}
}
}
複製程式碼
spring的程式碼風格好像有一個特點,凡是真正開始做事的方法入口都會以do為字首,經過前面一系列對xml配置檔案的設定,終於來到了doLoadBeanDefinitions(inputSource, encodedResource.getResource()),在這個方法裡Spring會讀取每一個Element標籤,並根據名稱空間找到對應的NameSpaceHandler去讀取解析Node生成BeanDefinition物件。經過一系列操作Resouse最終會被轉換成InputSource物件,這個類也沒什麼特別的,只是除了檔案流之外多了一些引數而已,比如XSD,DTD的publicId,systemId約束,檔案流的編碼等,最重要的還是InputStream,然後來看看這個方法:
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) {
//主要是校驗檔案 通過查詢DTD檔案約束 校驗檔案格式
//讀取xml檔案生成DOC
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
}
複製程式碼
這個方法完成兩件事情:
-
通過inputSource生成Document物件
-
解析Document並將生成BeanDefinition註冊到IOC中
4、解析DOC生成BeanDefinition
檔案校驗和生成DOC文件都是一些校驗操作,如果想自定義DTD文件讓Spring載入,後面還會細說這部分內容,暫且放下,現在主要是看看IOC的BeanDefinition的生成過程,接下來進入registerBeanDefinitions:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
documentReader.setEnvironment(this.getEnvironment());
//獲取載入之前IOC容器中的BeanDefinition數量
int countBefore = getRegistry().getBeanDefinitionCount();
//具體解析 註冊
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//返回本次載入的BeanDefinition數量
return getRegistry().getBeanDefinitionCount() - countBefore;
}
複製程式碼
在這個方法裡,首先建立BeanDefinitionDocumentReader,這是個介面用於完成BeanDefinition向IOC容器註冊的功能,Spring只提供了唯一的實現DefaultBeanDefinitionDocumentReader,檢視registerBeanDefinitions:
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
//root 在這個測試裡就是<beans></beans>
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}
複製程式碼
該方法第一步首先回去xml的根節點,在這個測試xml裡就是標籤了,然後將根節點作為引數傳入到下面的方法中解析doRegisterBeanDefinitions:
protected void doRegisterBeanDefinitions(Element root) {
//獲取profile環境變數
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
Assert.state(this.environment != null, "Environment must be set for evaluating profiles");
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
//判斷該root下的bean是否是前面通過web.xml或者前面設定的bean的環境值
//如果不是 不需要解析當前root標籤
if (!this.environment.acceptsProfiles(specifiedProfiles)) {
return;
}
}
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(this.readerContext, root, parent);
//解析bean所要執行的方法,該方法實際是空的 允許子類擴充套件 去讀取自定義的node
//屬於模板方法
preProcessXml(root);
//真正解析beans的方法
parseBeanDefinitions(root, this.delegate);
//beans解析完之後需要執行的方法,實際也是通過子類擴充套件 是模板方法
postProcessXml(root);
this.delegate = parent;
}
複製程式碼
這個方法看起來很多,其實真正核心的只有兩部分:
-
讀取beans的profile屬性,判斷是否屬於被啟用的組,如果不是則不解析
-
建立BeanDefinitionParserDelegate,委託該類執行beans解析工作。
最後通過parseBeanDefinitions(root, this.delegate)方法將beans的解析交給BeanDefinitionParserDelegate
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
//判斷是否是預設的名稱空間 也就是beans
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)) {
//判斷是否是xml配置的bean,如果是則呼叫該方法解析
parseDefaultElement(ele, delegate);
}
else {
//否則按照自定義方式解析
delegate.parseCustomElement(ele);
}
}
}
}
else {
//否則按照自定義方式解析
delegate.parseCustomElement(root);
}
}
複製程式碼
這個方法很重要,這裡已經開始解析了,首先會判斷,要解析的root是否是beans標籤,如果是再判斷子元素是否是元素,正常來講,我們使用spring的時候都會再標籤下配置,所以不出意外都會走到for迴圈裡,然後在for迴圈裡判斷是否是預設名稱空間的時候就會發生變化:
-
如果是則走parseDefaultElement(ele, delegate);
-
如果是
<mvc:annotation-driven>、 <context:component-scan base-package="***"/>
等則會走到自定義元素解析delegate.parseCustomElement(ele)裡
自定義解析載入到最後還是會跟載入預設名稱空間的bean一樣,所以在這裡只分析自定義名稱空間的解析,不過值得提一下的是自定義解析方法裡會首先根據Element的名稱空間找到NamespaceHandler,然後由該NamespaceHanler去解析
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
//獲取元素的名稱空間
String namespaceUri = getNamespaceURI(ele);
//獲取名稱空間解析器
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
//解析自定義元素
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
複製程式碼
由於後面會自己實現一個NamespaceHandler解析自定義的標籤,會專門說明Spring如何查詢NamespaceHandler以及如何解析自定義元素,這裡只是瞭解下NamespaceHandler的概念即可,接著看Spring解析
檢視parseDefaultElement:
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
//解析 import
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
//解析alias
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
//解析bean
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// 如果還是beans 遞迴呼叫
doRegisterBeanDefinitions(ele);
}
}
複製程式碼
這個方法裡面就是幾個if判斷,用於解析對應的標籤,其中import alias相當於是去讀取另一個xml檔案,最後還是會呼叫解析bean,所以在這裡只看解析bean的方法processBeanDefinition(ele, delegate):
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
//將bean的屬性都讀取到到BeanDefinitionHolder上
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
//如果bean裡面有自定義標籤 來決定是否再次解析
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// 將生成的BeanDefinitionHolder註冊到IOC中
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));
}
}
複製程式碼
在這個方法裡 ,之前分析的邏輯才逐漸清晰起來,程式碼的條例也很清晰
-
BeanDefinitionParserDelegate將bean標籤的屬性讀取到BeanDefinitionHolder物件中
-
如果beans下還有其他自定義標籤決定是否有必要再次解析
-
將BeanDefinition註冊到IOC中
-
傳送註冊事件
首先來看第一步,讀取node屬性到BeanDefinitionParserDelegate中
public AbstractBeanDefinition parseBeanDefinitionElement(
Element ele, String beanName, BeanDefinition containingBean) {
this.parseState.push(new BeanEntry(beanName));
String className = null;
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
//獲取class全限定名
className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
}
try {
String parent = null;
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
parent = ele.getAttribute(PARENT_ATTRIBUTE);
}
//設定beanClass或者beanClassName
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
//讀取node屬性 將配置的屬性 塞入合適的欄位中
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
parseMetaElements(ele, bd);
//記錄lookup-method配置
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
//記錄replaced-method配置
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
//解析建構函式(構造器注入)
parseConstructorArgElements(ele, bd);
//解析屬性(setter注入)
parsePropertyElements(ele, bd);
parseQualifierElements(ele, bd);
bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));
return bd;
}
finally {
this.parseState.pop();
}
return null;
}
複製程式碼
這個方法完成了讀取一個bean包括將node屬性讀入到BeanDefinition,讀取bean的建構函式配置(是構造器注入的前提),讀取bean的屬性配置(是setter注入的前提),其實將node屬性讀取到BeanDefinition很簡單,僅僅是一一對應而已,真正的複雜點在於讀取建構函式引數、讀取屬性值引數。
5、構造器和屬性引數解析
來看下面一段配置:
<bean id="userDao" class="spring.road.beans.models.UserDao"/>
<!--setter注入-->
<bean id="beanService" class="spring.road.beans.models.BeanService">
<property name="mapper" ref="userDao"/>
<property name="name" value="lijinpeng"/>
<property name="sex" value="false"/>
</bean>
<!--構造器注入-->
<bean id="person" class="spring.road.beans.models.Person">
<constructor-arg name="age" value="26"/>
<constructor-arg name="name" value="dangwendi"/>
<constructor-arg name="userDao" ref="userDao"/>
<constructor-arg name="sex" value="true"/>
</bean>
複製程式碼
這段配置使用了兩種注入方式:
property屬性解析
setter注入就是我們通過為屬性賦值,如果屬性值都是string型別的還很好解決,如果pojo類的屬性值不是String,而是比如像Boolean、int、Date等這些資料的時候,必須要進行資料轉換操作才可以在getBean()的時候將property配置的屬性通過反射注入到對應的欄位裡,這好像也不是什麼困難的事情,但是如果是ref引用型別呢,這個問題該如何解決呢?Spring很巧妙的解決了這個問題,用RuntimeBeanReference來表示ref引用的資料,用TypedStringValue表示普通String字串。既然一個pojo類的所有配置都會讀取到BeanDefinition,所以在xml中配置的屬性必然也會儲存到BeanDefinition中,繼續看原始碼會發現BeanDefinition中用MutablePropertyValues類表示屬性集合,該類中propertyValueList就是property集合資料,Spring用PropertyValue儲存了property的name value資訊。
//在BeanDefinition類中
MutablePropertyValues getPropertyValues();
//在MutablePropertyValues類中的屬性
private final List<PropertyValue> propertyValueList;
//在PropertyValue中的屬性
private final String name;
private final Object value;
複製程式碼
根據上面xml配置可以得知value可能需要型別轉換,也可能是引用ref,鑑於getBean階段無法直接賦值,所以需要一箇中間類儲存資料,在getBean()反射階段根據型別去轉換成物件,再次檢視parsePropertyElements方法:
public void parsePropertyElements(Element beanEle, BeanDefinition bd) {
NodeList nl = beanEle.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
//解析bean下的property屬性節點
if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) {
parsePropertyElement((Element) node, bd);
}
}
}
複製程式碼
parsePropertyElement解析bean下的property屬性節點
public void parsePropertyElement(Element ele, BeanDefinition bd) {
//獲取property的name 這個很簡單
String propertyName = ele.getAttribute(NAME_ATTRIBUTE);
if (!StringUtils.hasLength(propertyName)) {
error("Tag 'property' must have a 'name' attribute", ele);
return;
}
this.parseState.push(new PropertyEntry(propertyName));
try {
if (bd.getPropertyValues().contains(propertyName)) {
error("Multiple 'property' definitions for property '" + propertyName + "'", ele);
return;
}
//獲取獲取property的value 這個需要用中間類表示
Object val = parsePropertyValue(ele, bd, propertyName);
PropertyValue pv = new PropertyValue(propertyName, val);
parseMetaElements(ele, pv);
pv.setSource(extractSource(ele));
bd.getPropertyValues().addPropertyValue(pv);
}
finally {
this.parseState.pop();
}
}
複製程式碼
山重水複疑無路,柳暗花明又一村,下面方法即是實現過程:
public Object parsePropertyValue(Element ele, BeanDefinition bd, String propertyName) {
String elementName = (propertyName != null) ?
"<property> element for property '" + propertyName + "'" :
"<constructor-arg> element";
// Should only have one child element: ref, value, list, etc.
NodeList nl = ele.getChildNodes();
Element subElement = null;
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) &&
!nodeNameEquals(node, META_ELEMENT)) {
// Child element is what we're looking for.
if (subElement != null) {
error(elementName + " must not contain more than one sub-element", ele);
}
else {
subElement = (Element) node;
}
}
}
//是否有ref屬性
boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE);
//是否有value屬性
boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE);
//ref和value只能存在一個
if ((hasRefAttribute && hasValueAttribute) ||
((hasRefAttribute || hasValueAttribute) && subElement != null)) {
error(elementName +
" is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele);
}
if (hasRefAttribute) {
String refName = ele.getAttribute(REF_ATTRIBUTE);
if (!StringUtils.hasText(refName)) {
error(elementName + " contains empty 'ref' attribute", ele);
}
//如果是ref 則轉換成RuntimeBeanReference
RuntimeBeanReference ref = new RuntimeBeanReference(refName);
ref.setSource(extractSource(ele));
return ref;
}
else if (hasValueAttribute) {
//如果是String則轉換成TypedStringValue
TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE));
valueHolder.setSource(extractSource(ele));
return valueHolder;
}
else if (subElement != null) {
return parsePropertySubElement(subElement, bd);
}
else {
// Neither child element nor "ref" or "value" attribute found.
error(elementName + " must specify a ref or value", ele);
return null;
}
}
複製程式碼
看有註釋行的程式碼,結合上面的分析,大概能瞭解setter注入的第一階段BeanDefinition儲存屬性資料的方式了,在呼叫BeanFactory的getBean()方法時,在反射階段獲取到值物件時可以根據型別去獲取值,如果是TypedStringValue則只需校驗值是否應該轉換,如果需要轉換即可,至於如何轉換,如果是RuntimeBeanReference更簡單了,直接通過getBean()獲取就好了,請記住這兩個型別,在分析getBean()階段屬性值解析的時候就會用到他們。
建構函式constructor-arg解析
構造器注入其實比setter注入要稍微麻煩一點,之所以說麻煩其實就是要藉助相關技術去實現,因為構造器可能會有很多過載,在xml配置中如果引數順序不同可能會呼叫不同的建構函式,導致注入失敗,所以如果要儲存構造引數值,必須匹配到唯一合適的建構函式,並且在xml配置的constructor-arg必須按照一定規則與匹配的建構函式一一對應,才可以在getBean()階段注入成功。
當我自己嘗試去寫的時候,以為只需要通過反射獲取建構函式的引數名即可,但是很不幸,通過反射拿到的引數名是諸如arg1 arg2 這樣的name,所以只能通過讀取類的位元組碼檔案了, 以前看過《深入瞭解java虛擬機器》這本書,知道可以通過讀取位元組碼檔案的方式獲取引數名,但是裡面的各種索引,欄位表集合啊什麼的想記住真的好難,而且我的水平還遠遠達不到那個高度,所以就用現成的吧, 我當時是用javassite實現的,看了spring的原始碼,發現spring是用asm實現的,當然這個階段是在getBean()階段實現的,之所以介紹是因為必須要先了解為什麼Spring要這麼儲存構造引數,後面的getBean在分析這塊原始碼,還是先來看看建構函式引數在BeanDefinition的儲存吧。
spring允許在xml中通過index、name、type來指定一個引數,在BeanDefinition中使用ConstructorArgumentValues儲存建構函式引數集合,在ConstructorArgumentValues包含了兩個集合一個配置了索引的indexedArgumentValues引數集合,另一個沒有配置索引的genericArgumentValues建構函式引數集合,然後建構函式引數值用內部類ValueHolder表示,這個類裡包含了引數的value,型別,引數名等。
//在BeanDefinition類中
ConstructorArgumentValues getConstructorArgumentValues();
//在ConstructorArgumentValues類中
//使用了索引的建構函式引數值集合
private final Map<Integer, ValueHolder> indexedArgumentValues = new LinkedHashMap<Integer, ValueHolder>(0);
//未使用索引的建構函式引數值集合
private final List<ValueHolder> genericArgumentValues = new LinkedList<ValueHolder>();
//在ValueHolder中的屬性
private Object value;
private String type;
private String name;
private Object source;
複製程式碼
建構函式引數的儲存結構分析完了,接下來看看程式碼吧,其實儲存和屬性值的儲存是一樣的 ,這裡只看關鍵的程式碼:
public void parseConstructorArgElements(Element beanEle, BeanDefinition bd) {
NodeList nl = beanEle.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (isCandidateElement(node) && nodeNameEquals(node, CONSTRUCTOR_ARG_ELEMENT)) {
parseConstructorArgElement((Element) node, bd);
}
}
}
複製程式碼
解析constructor-arg標籤parseConstructorArgElement
public void parseConstructorArgElement(Element ele, BeanDefinition bd) {
//獲取index
String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE);
//獲取type
String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE);
//獲取name
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
//如果index不為空 儲存到indexedArgumentValues集合中
if (StringUtils.hasLength(indexAttr)) {
try {
int index = Integer.parseInt(indexAttr);
if (index < 0) {
error("'index' cannot be lower than 0", ele);
}
else {
try {
this.parseState.push(new ConstructorArgumentEntry(index));
//將value轉換成RuntimeBeanReference或者TypedStringValue
Object value = parsePropertyValue(ele, bd, null);
ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);
//儲存type
if (StringUtils.hasLength(typeAttr)) {
valueHolder.setType(typeAttr);
}
//儲存name
if (StringUtils.hasLength(nameAttr)) {
valueHolder.setName(nameAttr);
}
valueHolder.setSource(extractSource(ele));
if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) {
error("Ambiguous constructor-arg entries for index " + index, ele);
}
else {
//儲存建構函式引數值
bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder);
}
}
finally {
this.parseState.pop();
}
}
}
catch (NumberFormatException ex) {
error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele);
}
}
//index未配置 儲存到普通集合中genericArgumentValues
else {
try {
this.parseState.push(new ConstructorArgumentEntry());
Object value = parsePropertyValue(ele, bd, null);
ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);
if (StringUtils.hasLength(typeAttr)) {
valueHolder.setType(typeAttr);
}
if (StringUtils.hasLength(nameAttr)) {
valueHolder.setName(nameAttr);
}
valueHolder.setSource(extractSource(ele));
//儲存建構函式引數值
bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder);
}
finally {
this.parseState.pop();
}
}
}
複製程式碼
至此,一個xml配置的bean被完全存放到了BeanDefinition中,其實基於掃描註解配置也是一樣的,只不過在做很多清理工作,針對下面配置簡要說明下基於註解的處理:
<context:component-scan base-package="com.yms.market"/>
複製程式碼
-
首先spring讀取到這個node,會查詢該node的NameSpaceHandler,然後呼叫parse方法解析
-
然後讀取到屬性base-package,轉換成對應路徑後查詢該路徑下所有的class檔案
-
讀取class檔案的註解,檢視是否實現了特定註解,如果實現了註解則處裡方式與xml配置的處理相同,否則不處理。
真實的處理過程比較複雜,也是用了很多設計模式,用了很多類來處理,但是我想說的是,無論是用過註解還是通過xml,最終的處理方式都是一樣的,都是先生成一個BeanDefinition註冊到IOC中,然後通過getBean()獲取。
6、BeanDefinitionRegistry註冊
BeanDefinition建立完成了,還差最後一步,將生成的BeanDefinition註冊到IOC中,這就必須往回看了。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
//將bean的屬性都讀取到到BeanDefinitionHolder上
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
//如果bean裡面有自定義標籤 來決定是否再次解析
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// 將生成的BeanDefinitionHolder註冊到IOC中
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));
}
}
複製程式碼
這段程式碼應該還很熟悉,這次看最後一步registerBeanDefinition
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {
// 獲取beanName
String beanName = definitionHolder.getBeanName();
//註冊BeanDefinition,key為beanName,value是BeanDefinition
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
// 如果配置別名的話獲取別名
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String aliase : aliases) {
//註冊別名
registry.registerAlias(beanName, aliase);
}
}
}
複製程式碼
該方法完成了以下事情
- 將BeanDefinition用beanName作為key註冊到IOC容器中
- 如果配置了別名,將beanName與別名對映起來。
再來看具體的註冊過程registry.registerBeanDefinition,註冊是呼叫BeanDefinitionRegistry的registerBeanDefinition方法,在剛開始的分析說過DefaultListableBeanFactory實現了BeanDefinitionRegistry和BeanFactory,而且實現了具體邏輯,下面的內容就是Spring註冊的過程,為了看的清晰我省去了很多異常和無用的程式碼:
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
{
if (beanDefinition instanceof AbstractBeanDefinition) {
try {
//驗證
((AbstractBeanDefinition) beanDefinition).validate();
}
catch (BeanDefinitionValidationException ex) {
}
}
//這裡保證了執行緒安全
synchronized (this.beanDefinitionMap) {
BeanDefinition oldBeanDefinition = this.beanDefinitionMap.get(beanName);
if (oldBeanDefinition != null) {
if (!this.allowBeanDefinitionOverriding) {
//不允許覆蓋丟擲異常
}
else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) {
if (this.logger.isWarnEnabled()) {
}
else {
if (this.logger.isInfoEnabled()) {
}
}
}
else {
this.beanDefinitionNames.add(beanName);
this.frozenBeanDefinitionNames = null;
}
//註冊進去嘍
this.beanDefinitionMap.put(beanName, beanDefinition);
}
resetBeanDefinition(beanName);
}
複製程式碼
到此為止,IOC容器的第一步為bean生成BeanDefinition並註冊到IOC容器中完成,接下來就是第二步,通過BeanFactory實現依賴注入了。