首頁 > 軟體

IOC 容器啟動和Bean範例化兩個階段詳解

2022-08-11 18:01:03

IOC 容器的兩個階段

IOC 容器可以分為兩個階段 : 容器啟動階段和 Bean 範例化階段。

Spring 的 IoC 容器在實現的時候, 充分運用了這兩個階段的不同特點, 在每個階段都加入了相應的容器擴充套件點, 支援開發者根據具體場景的需要, 新增自定義的擴充套件邏輯.

容器啟動階段

容器啟動, 首先要載入設定後設資料 ( Configuration MetaData ).

容器使用工具類 BeanDefinitionReader 對載入的設定後設資料進行解析和分析, 並將分析後的資訊組裝為相應的 Bean 定義物件 BeanDefinition, 最後把這些儲存了 bean 定義必要資訊的 BeanDefinition, 註冊到相應的 BeanDefinitionRegistry, 這樣容器啟動工作就完成了. ( 將 XML 資訊對映到 BeanDefinition 物件上 )

BeanDefinition 物件中儲存的屬性很多,如下 :

BeanDefinitionRegister 介面用來註冊 BeanDefinition. 該介面的預設實現類為 DefaultListableBeanFactory. 在該實現類中, 有一個成員屬性定義如下 :

private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, bean>(64); 

BeanDefinition 就是被儲存到這個 map 中的, key 為 beanName, value 為 BeanDefinition 物件.

Bean 範例化階段

當呼叫者通過容器的 getBean() 方法明確地想要獲取某個物件, 或者因依賴關係, 容器需要隱式地呼叫 getBean() 方法時, 就會觸發容器的第二階段.

該階段, 容器會首先檢查所請求的物件之前是否已經初始化. 如果沒有, 則會根據註冊的 BeanDefinition 的資訊, 範例化這個物件 , 併為其注入依賴. 如果該物件實現了某些回撥介面, 也會根據回撥介面的要求來裝配它. 當該物件裝配完畢之後, 容器會立即將其返回給請求方去使用.

Bean 建立的步驟和普通物件的建立步驟不同, 普通的物件在建立時, 由類載入器找到 xxx.class 檔案, 然後建立 xxx 物件即可. Spring 中 bean 的建立多了一步, 先是由 類載入器找到 xxx.class 檔案, 然後將其解析組裝為 BeanDefinition 物件, 然後 Spring 根據 BeanDefinition 資訊來建立物件.

  • 普通物件 : xxx.class -> object
  • Bean 物件 : xxx.class -> BeanDefinition -> bean 物件

可以看到, Spring 在範例化物件時, 脫離了設定後設資料中的資訊, 而是使用的 BeanDefinition 中的資訊, 這就意味著我們可以修改 BeanDefinition, 從而改變 Spring 生成的物件的屬性, 甚至是修改最後生成的物件所屬的類.

插手容器的啟動

Spring 提供了一種叫做 BeanFactoryPostProcessor 的容器擴充套件機制. 該機制允許開發者在容器範例化相應 Bean 物件之前, 對註冊到容器的 BeanDefinition 的資訊做出修改. 即在容器的第一階段的最後進行一些自定義操作. 比如修改其中 bean 定義的某些屬性, 為 bean 定義增加其他資訊等.

Spring 中自帶的 BeanFactoryPostProcessor 介面的實現:

  • org.springframework.beans. factory.config.PropertyPlaceholderConfigurer預留位置機制
  • org.springframework.beans.factory. config.PropertyOverrideConfigurer 重寫屬性值
  • ...

同時也支援開發者通過實現 BeanFactoryPostProcessor 介面自定義擴充套件.

PropertyPlaceholderConfigurer 預留位置機制

PropertyPlaceholderConfigurer 允許開發者在 XML 組態檔中使用預留位置, 並將這些預留位置所代表的資源單獨設定到簡單的 properties 檔案中來載入.

比如將資料庫的連線資訊儲存在 properties 中.

<context:property-placeholder location="classpath:dbconfig.properties" />
<bean id="pooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
  <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
  <property name="driverClass" value="${jdbc.driverClass}"></property>
  <property name="user" value="${jdbc.user}"></property>
  <property name="password" value="${jdbc.password}"></property>
</bean>

基本流程 :

  • 當 BeanFactory 在第一階段載入完成所有設定資訊時, BeanFactory 中儲存的物件的屬性資訊還只是以預留位置的形式存在, 如 ${jdbc.url}, ${jdbc.driver}.
  • 當 PropertyPlaceholderConfigurer 作為 BeanFactoryPostProcessor 被應用時, 它會使用 properties 組態檔中的設定資訊來替換相應 BeanDefinition 中預留位置所表示的屬性值.
  • 這樣, 當進入容器實現的第二階段範例化 bean 時, bean 定義中的屬性值就是最終替換完成的了.

PropertyOverrideConfigurer 重寫屬性值

PropertyOverrideConfigurer 允許你對容器中設定的任何你想處理的 bean 定義的 property 資訊進行覆蓋替換.

PropertyOverrideConfigurer 的 properties 檔案中的設定項, 可以覆蓋掉原來 XML 中的 bean 定義的 property 資訊.

範例-使用容器擴充套件點修改 BeanDefinition

此案例使用註解的方式來設定後設資料.

現在有兩個類, 一個是 User 類, 一個是 Good 類, User 類歸 Spring 管理, 而 Good 類並不歸 Spring 管理. 類的定義如下 :

@Data
@AllArgsConstructor
@Accessors(chain = true)
@Component              //被 Spring 掃描
public class User {
    private int id;
    private String name;
    public User(){
        System.out.println("呼叫了 user 的無引數的構造方法");
    }
}
@Data
@AllArgsConstructor
@Accessors(chain = true)
public class Good {
    private int id;
    private String name;
    public Good(){
        System.out.println("呼叫了 good 的無引數的構造方法");
    }
}

AppConfig 類進行註解的掃描.

@ComponentScan(value = {"it.com"})
public class AppConfig {
}

Test 類用來測試 :

public static void main(String[] args){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
    User user = (User) context.getBean("user");
    Good good = (Good) context.getBean("good");
}

執行測試類, 結果如下 :

User 類設定了被 Spring 掃描, 所以可以獲取到 user 物件, 而 Good 類沒有設定被掃描, 所以無法獲取 good 物件, 而報錯.

現在我們要做的就是, 讓沒有被 Spring 管理的 Good 類, 也能從 Spring 容器中獲取到它的範例. 實現上述目的的思路就是, 對容器啟動的第一階段生成的 User 類的 BeanDefinition 類做修改.

建立自定義的 BeanFactoryPostProcessor 如下 :

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor{
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 通過 Spring 中 bean 的名字獲取到指定的 bean
        GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanFactory.getBeanDefinition("user");
        // 輸出 beanDefinition 的資訊
        System.out.println(beanDefinition.getBeanClass());
        System.out.println(beanDefinition.getFactoryBeanName());
        // 然後進行狸貓換太子,將 User 類對應的 BeanDefinition 物件的 beanClass 屬性替換掉
        beanDefinition.setBeanClass(Good.class);
    }
}

然後再次執行測試類, 結果如下 :

可以看到,雖然表面上是通過 getBean("user") 來獲取 user 物件,但是實際呼叫的確實 Good 物件的構造方法,返回的是 good 物件. 但 Good 並沒有讓 Spring 掃描. 這個例子就展示瞭如何通過 BeanFactoryPostProcessor 機制插手容器的啟動

以上就是IOC 容器啟動和Bean範例化兩個階段詳解的詳細內容,更多關於IOC 容器啟動Bean範例化的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com