接前面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