结合配置文件、反射完善控制反转(IoC)、依赖注入(DI)http://haolloyin.blog.51cto.com/1177454/463673
接前面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