結合配置文件、反射完善控制反轉(IoC)、依賴注入(DI)

 接前面2篇“演進式例解控制反轉(IoC)、依賴注入(DI之一”和“演進式例解控制反轉(IoC)、依賴注入(DI之二”的例子繼續往下。

 
回顧:

前面兩篇文章雖然漸進式地引出了 IoC DI,但那些都是硬編碼在源代碼中的,靈活性非常糟糕,每次修改組件依賴的配置之後都得重新編譯、部署。

 
問題描述:
如“回顧”所指,如何能夠使具體組件依賴的配置脫離源代碼存在?需要將這種硬編碼的僵化設計改進爲可靈活熱插拔的方式。
 
解決方案:

可以使用我們常見的運行時讀取配置文件來管理組件間的依賴性,然後再結合反射技術實現依賴注入。在 Java 裏面,除了 XML 文件還有鍵-值對形式的 .properties 屬性文件可以使用。

 

問題在於, .properties文件中定義怎樣一種合適的格式來方便程序從中獲取組件依賴信息並以此進行注入?

 

在我們這個簡單的實現中,對 .properties 文件制定如下兩種簡單定義:

♢ 普通對象名(首字母小寫)=完整類名(含包名),指定應該被反射實例化的類實例,描述一個組件的定義

♢ 普通對象名.字段名(首字母小寫)=.properties文件中已經定義的組件定義,描述依賴注入的定義。注意有個點 . 哦!

 
於是,可以得出如下配置文件格式,這也是下面例子中要用到的配置文件:

# define a new concrete bean'reportGenerator'

reportGenerator=IoC_DI.use_reflect.PDFGenerator 

# define a new concrete report service'reportService'

reportService=IoC_DI.use_reflect.ReportService 

# inject the bean 'reportGenerator' into the 'reportService'

reportService.reportGenerator=reportGenerator

 
實現方法:
在上一篇文章的基礎上,因爲需要容器加載外部的.properties文件進行配置管理,結合反射進行組件實例化、注入,所以在這裏要自己實現一個非常簡單的setter方式的依賴注入工具,稱之爲BeanUtil類。
 

BeanUtil.java反射、注入工具類代碼如下,請詳看註釋:

  1. package IoC_DI.use_reflect; 
  2.  
  3. import java.lang.reflect.Method; 
  4.  
  5. public class BeanUtil { 
  6.  
  7.     /** 
  8.      * 利用反射進行依賴注入 
  9.      * @param bean 需要注入外部依賴的主體類實例 
  10.      * @param fieldName 需要注入的字段名 
  11.      * @param fieldRef 被注入的組件實例 
  12.      * @throws Exception 
  13.      */ 
  14.     public static void setProperty(Object bean, String fieldName, 
  15.             Object fieldRef) throws Exception { 
  16.  
  17.         // 獲取主體類的完整名稱 
  18.         String className = getClassName(bean); 
  19.  
  20.         // 獲取主體類的所有 Method 
  21.         Class beanClass = Class.forName(className); 
  22.         Method[] methods = beanClass.getMethods(); 
  23.  
  24.         // 準備對應 setter()方法的完整名稱 
  25.         String setterName = "set" + fieldName.substring(01).toUpperCase() 
  26.                 + fieldName.substring(1, fieldName.length()); 
  27.  
  28.         // 遍歷找到對應 setter 方法,並調用 invoke()方法進行注入 
  29.         for (Method m : methods) { 
  30.             if (m.getName().equals(setterName)) { 
  31.                 m.invoke(bean, fieldRef); 
  32.                 System.out.println("已調用 " + m.getName() + "() 向 " + className 
  33.                         + " 注入 " + getClassName(fieldRef)); 
  34.                 return
  35.             } 
  36.         } 
  37.         System.out.println(">>注入失敗: " + className + "類中不存在" + fieldName 
  38.                 + "字段對應的setter()方法 ..."); 
  39.     } 
  40.  
  41.     /** 
  42.      * 根據 Object 實例獲取類的完整名稱 
  43.      * @param o 
  44.      * @return 
  45.      */ 
  46.     private static String getClassName(Object o) { 
  47.         if (o == null) { 
  48.             System.out.println("傳入的 Object 實例爲 null ..."); 
  49.             return null
  50.         } 
  51.         String fullName = o.toString(); 
  52.         String className = fullName.substring(0, fullName.indexOf("@")); 
  53.         return className; 
  54.     } 

 

對於原來的容器 Container 類,也需要相應的修改,主要體現在:

Container 初始化時加載外部 .properties 配置文件,不再構造器中硬編碼實例化各個組件並進行依賴注入。

Container 加載 .properties 配置文件之後自己解析該文件內容,即遍歷其中的所有鍵-值條目,決定如何處理組件定義、依賴注入。

 

在這個例子中,我將配置文件命名爲bean_config.properties 其內容即爲前面給出的那樣。

修改後的 Container.java 詳細代碼如下:

  1. class Container { 
  2.     // 以鍵-值對形式保存各種所需組件 Bean 
  3.     private static Map<String, Object> beans; 
  4.  
  5.     public Container() { 
  6.         System.out.println("1...開始初始化 Container ..."); 
  7.  
  8.         beans = new HashMap<String, Object>(); 
  9.          
  10.         try { 
  11.             Properties props = new Properties(); 
  12.             props.load(new FileInputStream("bean_config.properties")); 
  13.              
  14.             for(Map.Entry entry : props.entrySet()) { 
  15.                 String key = (String)entry.getKey(); 
  16.                 String value = (String)entry.getValue(); 
  17.                 // 處理 key-value,進行依賴屬性的注入 
  18.                 this.handleEntry(key, value); 
  19.             } 
  20.         } catch (Exception e) { 
  21.             e.printStackTrace(); 
  22.         } 
  23.  
  24. //      // 創建、保存具體的報表生起器 
  25. //      ReportGenerator reportGenerator = new PDFGenerator(); 
  26. //      beans.put("reportGenerator", reportGenerator); 
  27. // 
  28. //      // 獲取、管理 ReportService 的引用 
  29. //      ReportService reportService = new ReportService(); 
  30. //      // 注入上面已創建的具體 ReportGenerator 實例 
  31. //      reportService.setReportGenerator(reportGenerator); 
  32. //      beans.put("reportService", reportService); 
  33.  
  34.         System.out.println("5...結束初始化 Container ..."); 
  35.     } 
  36.  
  37.     /** 
  38.      * 根據key-value處理配置文件,從中獲取bean及其依賴屬性並注入 
  39.      * @param key 
  40.      * @param value 
  41.      * @throws Exception 
  42.      */ 
  43.     private void handleEntry(String key, String value) throws Exception { 
  44.         String [] keyParts = key.split("\\."); 
  45.          
  46.         if(keyParts.length == 1) { 
  47.             // 組件定義:利用反射實例化該組件 
  48.             Object bean = Class.forName(value).newInstance(); 
  49.             beans.put(keyParts[0], bean); 
  50.         }else { 
  51.             // 依賴注入:獲取需要bean的主體,以及被注入的實例 
  52.             Object bean = beans.get(keyParts[0]); 
  53.             Object filedRef = beans.get(value); 
  54.             BeanUtil.setProperty(bean, keyParts[1], filedRef); 
  55.         } 
  56.     } 
  57.      
  58.     public static Object getBean(String id) { 
  59.         System.out.println("最後獲取服務組件...getBean() --> " + id + " ..."); 
  60.         return beans.get(id); 
  61.     } 
 
根據以上具體配置文件,運行得到結果如下:

1...開始初始化 Container ...

2...開始初始化 PDFGenerator ...

3...開始初始化 ReportService ...

4...開始注入 ReportGenerator ...

已調用 setReportGenerator() IoC_DI.use_reflect.ReportService 注入 IoC_DI.use_reflect.PDFGenerator

5...結束初始化 Container ...

最後獲取服務組件...getBean() --> reportService ...

generate an PDF report ...

 
想要使用其他逐漸,只要修改配置文件中第一個組件定義爲:

# define a new concrete bean 'reportGenerator'

reportGenerator=IoC_DI.use_reflect.ExcelGenerator

 
運行結果如下:

1...開始初始化 Container ...

2...開始初始化 ExcelGenerator ...

3...開始初始化 ReportService ...

4...開始注入 ReportGenerator ...

已調用 setReportGenerator() IoC_DI.use_reflect.ReportService 注入 IoC_DI.use_reflect.ExcelGenerator

5...結束初始化 Container ...

最後獲取服務組件...getBean() --> reportService ...

generate an Excel report ...

 
 

注意:

♢ 在文中的這個例子當中,BeanUtil只是非常簡單地實現了setter方式的依賴注入,甚至沒有參數檢查、異常處理等。

♢  Container 類中的私有輔助方法handleEntry() 中,發現對於組件定義和依賴注入的情況有不同的處理。前者組件定義是在該方法內使用反射進行實例化,並添加到beans當中,如下:

if(keyParts.length == 1) {

    // 組件定義:利用反射實例化該組件

    Object bean = Class.forName(value).newInstance();

    beans.put(keyParts[0], bean);

}

 

而對於依賴注入,則委託BeanUtil類來完成反射、實例化並注入,代碼如下:

else {

    // 依賴注入:獲取需要bean的主體,以及被注入的實例

    Object bean = beans.get(keyParts[0]);

    Object filedRef = beans.get(value);

    BeanUtil.setProperty(bean, keyParts[1], filedRef);

}

 

在這裏我想說的是,好像這樣子的設計有點問題,因爲關於反射這種細節實現被分開在兩個地方(Container 類和 BeanUtil 類),也就是說 BeanUtil 工具類的功能還不夠全面,可以再提供一個方法將上面第一種情況委託給 BeanUtil 來完成,實現職責的統一。

 

後記:

實際上,在《Spring攻略》中作者是使用Apache Commons項目的一個開源工具包commons-beanutils來操作 .properties 配置文件的。而我,最初也是按照其建議使用這個包的,可是運行時總是拋出NoSuchMethodException 異常Property 'reportGenerator' has no setter method in class 'class IoC_DI.use_reflect.ReportService'Eclipse自動生成的setter未能解決該問題,自己查看commons-beanutils 包對應類的源代碼也沒無果。猜測問題可能出在commons-beanutils 包對應類好像使用了麻煩的描述符來查找 setter 方法。最後還是自己實現一下更加輕快:-D

 

回頭看看上一篇文章,應該更能幫助理清例子的演進歷程:-D

演進式例解控制反轉(IoC)、依賴注入(DI

 
以下文章你可能也會感興趣:

Factory Method)工廠方法模式的Java實現

Java RMI 框架的工廠方法模式實現

Mediator)中介者模式的Java實現(加修改)

Dynamic Proxy)動態代理模式的Java實現

Template Method)模板方法模式的Java

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章