深入理解Spring的Bean定義物件BeanDefinition-面試重點

程式設計師xiaozhang發表於2023-02-28

Spring註解這篇文章中講到了Spring的元件,元件載入到Spring容器中也就是Spring容器中的Bean物件,想要更深理解Spring中的Bean物件,那對這個BeanDefinition一定要有深入的瞭解,它是構造出來Bean物件的一切基礎,比如Bean的作用域,Bean的注入模型,Bean是否進行載入等等資訊,都需要一個BeanDefinition類來定義描述這些Bean的資訊。如下Spring對BeanDefinition的描述:

簡單翻譯就是BeanDefinition描述了Bean的例項,該例項具有的屬性值,建構函式引數值。有興趣這個類的可以檢視Spring原始碼,列舉部分方法,方法太多如果想看全部,請直接看Spring原始碼

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
  /**
   * Scope identifier for the standard singleton scope: "singleton".
   */
  String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON;
  /**
   * Scope identifier for the standard prototype scope: "prototype".
   */
  String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE;
  /**
   * Specify the bean class name of this bean definition.
   */
  void setBeanClassName(@Nullable String beanClassName);
  /**
   * Return the current bean class name of 
   *  this bean definition.
   */
  @Nullable
  String getBeanClassName();
  /**
   * Override the target scope of this bean, specifying
   * a new scope name.E
   */
  void setScope(@Nullable String scope);
  /**
   * Return the name of the current target scope for this bean,
   * or {@code null} if not known yet.
   */
  @Nullable
  String getScope();
  /**
   * Set whether this bean should be lazily initialized.
   */
  void setLazyInit(boolean lazyInit);
  /**
   * Return whether this bean should be lazily initialized, 
   *  i.e. not eagerly instantiated on startup. 
   * Only applicable to a singleton bean.
   */
  boolean isLazyInit();

如果還不是很理解,那我們可以做一個對比。Java中的Class大家都很熟悉。Class可以用來描述一個類的屬性和方法等等其他資訊,而BeanDefintion可以描述Bean當中的scope,lazy,以及屬性和方法等等其他資訊

對上圖的文字說明一下:假設磁碟上有1個.java檔案,首先我們把這些java檔案編譯成class檔案,繼而java虛擬機器啟動會把這些class檔案載入到記憶體,當遇到new關鍵字的時候會根據類的模板資訊例項化這個物件也就是在堆上面分配記憶體。

但是Spring的Bean例項化過程和一個普通java物件的例項化過程還是有區別的,同樣用一幅圖來說明一下

先解釋一下對Bean重定義能做些什麼事情,比如你想吃麵包但是兜裡的錢只能買一個饅頭,但是經過對饅頭一些重新定義,你可以把饅頭變成麵包,麵包吃著是比饅頭香【PS為了能夠理解只是做一個通俗的比方,年輕人還是要好好掙錢,爭取買的起麵包吃,後面會舉一個列子】。然後對每個步驟進行解釋如下。

當Spring容器啟動的時候會去呼叫ConfigurationClassPostProcessor這個Bean工廠的後置處理器完成掃描,其實所謂的Spring掃描就是把類的資訊讀取到,比如類的型別(class),比如類的名字,類的構造方法。可能大家會有疑問這些資訊不需要存,直接存在class物件裡面不就可以?比如當Spring掃描到Student的時候Class clazz = Student.class;那麼這個class裡面就已經具備的前面說的那些資訊了,確實如此。但是Spring例項化一個Bean不僅僅只需要這些資訊,還有我上文說到的scope,lazy等等資訊需要儲存,所以Spring設計了一個BeanDefintion的類用來儲存這些資訊。故而當Spring讀取到類的資訊之後會例項化一個BeanDefinition的物件,繼而呼叫這個物件的各種set方法儲存資訊;每掃描到一個符合規則的類,Spring都會例項化一個BeanDefinition物件,然後把根據類的類名生成一個Bean的名字(比如一個類UserService,Spring會根據類名UserService生成一個Bean的名字userService,Spring內部有一套預設的名字生成規則,但是程式設計師可以重寫覆蓋這個規則,然後Spring會把這個beanDefinition物件和生成的beanName放到一個map當中,key=beanName,value=beanDefinition物件。

當Spring把所有對應類的beanDefintion物件存到map之後,Spring會呼叫bean工廠後置處理器BeanFactoryPostProcessor【PS,Spring內部很多這個類的實現類,我們程式設計師也可以自己實現這個類】,這個是很重要類,如下圖在Spring給它的定義:

 

主要意思就是:在應用程式上下文的標準初始化之後修改它的內部Bean工廠。

可以看到BeanFactoryPostProcessor裡唯一的方法postProcessBeanFactory中唯一的引數就是一個標準的beanFactory物件——ConfigurableListableBeanFactory,Spring在呼叫postProcessBeanFactory方法的時候把已經例項化好的beanFactory物件傳過來了,那麼我們可以對這個beanFactory肆意妄為了。我們上文說的饅頭變麵包也就是在這一個步驟執行的。下面我們就寫一個程式碼把饅頭變麵包。

1:先建3個類,為啥是三個因為商家善良買饅頭的時候送你一個燒餅
package io.renren.service;
import org.springframework.stereotype.Service;
//饅頭加入Spring容器了
@Service
public class ManTou {
}
package io.renren.service;
//燒餅沒加入spring容器
public class ShaoBing {
}

package io.renren.service;
//麵包也沒加入Spring容器
public class MianBao {
}

//2:饅頭變成麵包程式碼如下:
package io.renren.service;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.stereotype.Component;
@Component
public class MyBeanFactoryPostPorcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {   
        //轉換為子類,因為父類沒有新增beanDefintion物件的api
        DefaultListableBeanFactory defaultbf = (DefaultListableBeanFactory) beanFactory;
        //new一個beanDefinition物件,這是隱含的送了一個燒餅
        GenericBeanDefinition shaoBing= new GenericBeanDefinition();
        shaoBing.setBeanClass(ShaoBing.class);
        //新增一個beanDefinition物件,原本這個Y沒有被spring掃描到
        defaultbf.registerBeanDefinition("shaoBing", shaoBing);
       // **********************上面是其他作用,買饅頭的時候商家善良給你一個燒餅*****************

        // **************下面是饅頭變麵包*******************
        //得到一個已經被掃描出來的beanDefintion物件x
        //因為饅頭本來就被掃描出來了,所以是直接從map中獲取
        BeanDefinition manTou = defaultbf.getBeanDefinition("manTou");
        //修改這個饅頭的c物件的class為麵包
        //原本這個mantou代表的class為manTou.class;現在為mianMao.class
        manTou.setBeanClassName("io.renren.service.MianBao");
    }
}

上面的程式碼主要有2個用途

1:買饅頭送了一個燒餅【PS想送燒餅只要加入一個燒餅的BeanDefinition物件就可以了,這也說明了BeanDefinition的重要性】。

2:把饅頭換成了麵包也就是偷樑換柱改變饅頭的BeanDefinition就可以了。

然後執行如下圖,我們明明放入Spring容器中的是饅頭,結果饅頭沒有獲取到,得到了燒餅和麵包。

如果說上面感覺用途不太大。上面的用途很廣,大名鼎鼎的spring-mybatis就用了上面的原理了,mybatis利用了spring的BeanDefinitionRegistryPostProcessor這個擴充套件點,去把我們程式設計師提供的mapper介面,放入到了beanDefinitionMap中。

 

相關文章