接前面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 |
BeanUtil.java反射、注入工具類代碼如下,請詳看註釋:
- package IoC_DI.use_reflect;
- import java.lang.reflect.Method;
- public class BeanUtil {
- /**
- * 利用反射進行依賴注入
- * @param bean 需要注入外部依賴的主體類實例
- * @param fieldName 需要注入的字段名
- * @param fieldRef 被注入的組件實例
- * @throws Exception
- */
- public static void setProperty(Object bean, String fieldName,
- Object fieldRef) throws Exception {
- // 獲取主體類的完整名稱
- String className = getClassName(bean);
- // 獲取主體類的所有 Method
- Class beanClass = Class.forName(className);
- Method[] methods = beanClass.getMethods();
- // 準備對應 setter()方法的完整名稱
- String setterName = "set" + fieldName.substring(0, 1).toUpperCase()
- + fieldName.substring(1, fieldName.length());
- // 遍歷找到對應 setter 方法,並調用 invoke()方法進行注入
- for (Method m : methods) {
- if (m.getName().equals(setterName)) {
- m.invoke(bean, fieldRef);
- System.out.println("已調用 " + m.getName() + "() 向 " + className
- + " 注入 " + getClassName(fieldRef));
- return;
- }
- }
- System.out.println(">>注入失敗: " + className + "類中不存在" + fieldName
- + "字段對應的setter()方法 ...");
- }
- /**
- * 根據 Object 實例獲取類的完整名稱
- * @param o
- * @return
- */
- private static String getClassName(Object o) {
- if (o == null) {
- System.out.println("傳入的 Object 實例爲 null ...");
- return null;
- }
- String fullName = o.toString();
- String className = fullName.substring(0, fullName.indexOf("@"));
- return className;
- }
- }
對於原來的容器 Container 類,也需要相應的修改,主要體現在:
♢ Container 初始化時加載外部 .properties 配置文件,不再構造器中硬編碼實例化各個組件並進行依賴注入。
♢ Container 加載 .properties 配置文件之後自己解析該文件內容,即遍歷其中的所有鍵-值條目,決定如何處理組件定義、依賴注入。
在這個例子中,我將配置文件命名爲bean_config.properties ,其內容即爲前面給出的那樣。
修改後的 Container.java 詳細代碼如下:
- class Container {
- // 以鍵-值對形式保存各種所需組件 Bean
- private static Map<String, Object> beans;
- public Container() {
- System.out.println("1...開始初始化 Container ...");
- beans = new HashMap<String, Object>();
- try {
- Properties props = new Properties();
- props.load(new FileInputStream("bean_config.properties"));
- for(Map.Entry entry : props.entrySet()) {
- String key = (String)entry.getKey();
- String value = (String)entry.getValue();
- // 處理 key-value,進行依賴屬性的注入
- this.handleEntry(key, value);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- // // 創建、保存具體的報表生起器
- // ReportGenerator reportGenerator = new PDFGenerator();
- // beans.put("reportGenerator", reportGenerator);
- //
- // // 獲取、管理 ReportService 的引用
- // ReportService reportService = new ReportService();
- // // 注入上面已創建的具體 ReportGenerator 實例
- // reportService.setReportGenerator(reportGenerator);
- // beans.put("reportService", reportService);
- System.out.println("5...結束初始化 Container ...");
- }
- /**
- * 根據key-value處理配置文件,從中獲取bean及其依賴屬性並注入
- * @param key
- * @param value
- * @throws Exception
- */
- private void handleEntry(String key, String value) throws Exception {
- String [] keyParts = key.split("\\.");
- if(keyParts.length == 1) {
- // 組件定義:利用反射實例化該組件
- Object bean = Class.forName(value).newInstance();
- beans.put(keyParts[0], bean);
- }else {
- // 依賴注入:獲取需要bean的主體,以及被注入的實例
- Object bean = beans.get(keyParts[0]);
- Object filedRef = beans.get(value);
- BeanUtil.setProperty(bean, keyParts[1], filedRef);
- }
- }
- public static Object getBean(String id) {
- System.out.println("最後獲取服務組件...getBean() --> " + id + " ...");
- return beans.get(id);
- }
- }
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
(Template Method)模板方法模式的Java實現