最新完整代码在:http://code.taobao.org/svn/bigfoot/trunk/java/src/ajaxjs/lang/ioc/。
承蒙《自己动手写一个ioc工具》一文指点,尚知依赖注射(Dependency Injection,简称 DI)之一二,为 Java 对象解耦之必备良品。却说“依赖注射”,众人即推 Spring、Guice 为大宗,诚然,此类框架发轫之初,便用途甚广,早已深入民心。若自己编码实现,却倒也没有想过。现今,经此文配源码一一介绍,方知所谓实现“依赖注释的原理”是此等简单的,于是,忍不住手有纳入库的想法。
就“依赖注射”本身而言,原不是 Java 之必须,然,目下所及多少 Java Web 框架,推广之时都有带上“内置 Ioc”功能云云,可想此特性乃框架之必备矣。然而,此概念于我而言,却是鲜有使用经验——即使本人翻阅过许多资料,仍不能理解得十分透彻,呜呼,本人资质愚钝,真不忍直视。
不料,拜此文作者所赐,偶得 DI 全部源码,写得却不算复杂,正好迎合本人简单学的初衷,逐有一举拿下、为我所用之态势。
怎么个简单的法?
——此 DI “框架”只有两个类和两个注解,除却了测试文件,只有四个 .java 文件,代码行数极少。下面一一介绍(尽管就是 copy & paste)。
首先是 Scanner.java 扫描类,如作者所言“ spring 中的,我就直接拿来用”——主要是扫描特定目录下 Java 文件中需要注射的和被注射的。扫描后返回 Set<Class<?>> 集合供 BeanContext 分析用。Scanner.java 代码在 SVN 仓库。
package ajaxjs.lang.ioc; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.URL; import java.net.URLDecoder; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.Set; /** * spring 中的,我就直接拿来用 * @author spring * */ public class Scanner { private ClassLoader classLoader = null; /** * 扫描包下面的类 * @param packageName */ public Set<Class<?>> scanPackage(String packageName) { classLoader = Thread.currentThread().getContextClassLoader(); // 是否循环搜索包 boolean recursive = true; // 存放扫描到的类 Set<Class<?>> classes = new LinkedHashSet<Class<?>>(); // 将包名转换为文件路径 String packageDirName = packageName.replace('.', '/'); try { Enumeration<URL> dirs = classLoader.getResources(packageDirName); while (dirs.hasMoreElements()) { URL url = dirs.nextElement(); String protocol = url.getProtocol(); if ("file".equals(protocol)) { // 获取包的物理路径 String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); // 以文件的方式扫描整个包下的文件 并添加到集合中 findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes); } } return classes; } catch (IOException e) { e.printStackTrace(); } return null; } /** * 以文件的形式来获取包下的所有Class * @param packageName 包名 * @param packagePath 包的物理路径 * @param recursive 是否递归扫描 * @param classes 类集合 */ private void findAndAddClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, Set<Class<?>> classes) { // 获取此包的目录 建立一个File File dir = new File(packagePath); // 如果不存在或者 也不是目录就直接返回 if (!dir.exists() || !dir.isDirectory()) { System.out.println("用户定义包名 " + packageName + " 下没有任何文件"); return; } // 如果存在 就获取包下的所有文件 包括目录 File[] dirFiles = dir.listFiles(new FileFilter() { // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件) public boolean accept(File file) { return (recursive && file.isDirectory()) || (file.getName().endsWith(".class")); } }); // 循环所有文件 for (File file : dirFiles) { // 如果是目录 则递归继续扫描 if (file.isDirectory()) { findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes); } else { // 如果是java类文件 去掉后面的.class 只留下类名 String className = file.getName().substring(0, file.getName().length() - 6); try { // 添加到集合中去 classes.add(classLoader.loadClass(packageName + '.' + className)); } catch (ClassNotFoundException e) { System.out.println("添加用户自定义视图类错误 找不到此类的.class文件"); e.printStackTrace(); } } } } }
BeanContext 本身为单例(Singleton),乃分析依赖关系并储存之,结构亦十分简单,主要一下组成:
// 存放bean private Map<String, Object> beans = new HashMap<String, Object>(); // 记录依赖关系 private Map<String, String> dependencies = new HashMap<String, String>();
依赖和被依赖的对象都为 Bean,有 getter/setter 方法,我们就是通过 setXXX() 方法注入所依赖的对象。Bean 在 test 目录有两个例子 Hi.java 和 Person.java 两个 Bean 类(标注了 @Bean("hi") 和 @Bean("jack")——尽管这里 @Bean("jack") 是 jack,但 jack 实际为 Person 类之 id 而已,真正需要返回的值是 private String name = "Rose";),分别为动词 hi 和名词 Person,而 Person 又作为依赖的对象注入到 hi 动作中。怎么确定这些依赖关系呢?答案就在 BeanContext.java 中!按作者原文所言:
分析注解及依创建对象,注入依赖
遍历类集合,如果检测到有
@Bean
注解则实例化对象存放到Map中,然后继续扫描该类下的所有field,如果发现@Resource
注解则记录依赖值Map中。
BeanContext.java 代码在 SVN 仓库,首先是得到所有 bean 的 Class 对象加以分析,采取的手段是“反射”。
/** * 扫描注解,创建对象,记录依赖关系 * @param classes 类集合 */ private void createBeansAndScanDependencies(Set<Class<?>> classes) { Iterator<Class<?>> iterator = classes.iterator(); while (iterator.hasNext()) { Class<?> item = iterator.next(); Bean annotation = item.getAnnotation(Bean.class); if (annotation != null) { String beanName = annotation.value(); try { this.beans.put(beanName, item.newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } /* * 记录依赖关系 */ Field[] fields = item.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { Field field = fields[i]; Resource fieldAnnotation = field.getAnnotation(Resource.class); if (fieldAnnotation != null) { // 获取依赖的bean的名称,如果为null,则使用字段名称 String resourceName = fieldAnnotation.value(); if (ajaxjs.Util.isEmptyString(resourceName))resourceName = field.getName(); this.dependencies.put(beanName + "." + field.getName(), resourceName); } } } } }
其中,this.beans.put(beanName, item.newInstance()); 就是实例化各个 bean 对象并以注解定义的 beanName 为 key 保存到 BeanContext.beans 中。
分析依赖关系,仍是“反射”,见 Resource fieldAnnotation = field.getAnnotation(Resource.class);。
经过反射之后,获得 依赖关系 和 resourceName。所谓依赖关系,是形如 hi.person 的字符串;resourceName 是注解中的值,如果没有则使用字符名称(没有的话这时要把 Bean(id) 的 id 设为类名,此处即 person,小写的)——当前例子是 bean 对象 Person 的 id,Jack。上述两者组成了 this.dependencies。
得到关系之后,便是进行注射了,见 private void injectBeans() 方法。
/** * 扫描依赖关系并注入bean */ private void injectBeans() { /* Iterator<Map.Entry<String, String>> iterator = dependencies.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, String> item = iterator.next(); String key = item.getKey(); String value = item.getValue();// 依赖对象的值 String[] split = key.split("\\.");// 数组第一个值表示bean对象名称,第二个值为字段属性名称 setProperty(beans.get(split[0]), split[1], beans.get(value)); } */ for(String key : dependencies.keySet()){ String value = dependencies.get(key);// 依赖对象的值 String[] split = key.split("\\.");// 数组第一个值表示bean对象名称,第二个值为字段属性名称 setProperty(beans.get(split[0]), split[1], beans.get(value)); } }
注意,我将原作者的迭代器改为 keySet 遍历,语法上感觉更简单。而 setProperty() 方法,我更是抛弃原来 commons.apache.beanutils 的 PropertyUtils.setProperty(),做到 JAR 包零依赖。
private void setProperty(Object bean, String name, Object value){ String setMethodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1); Class<?> clazz = bean.getClass(); Method method = null; try { method = clazz.getDeclaredMethod(setMethodName, value.getClass()); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } try { method.invoke(bean, value); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
setProperty 无非就是一些反射手段调用 setXXX() 方法,相当于“注射”的过程。被注射的对象 Object value 之前早已实例化在 this.beans 中——通过 beans.get(value) 返回(实际是通过 Hash 取值)。
这样,整个依赖分析过程就完成了。
***********使用方法**************
Set<Class<?>> classes = new Scanner().scanPackage("ajaxjs.lang.ioc.test"); BeanContext.me().init(classes); Hi hi = (Hi) BeanContext.me().getBean("hi"); hi.sayHello();
2015-7-22:被注入的类可以不限定某个类了,能够支持父类和接口!详见更新的源码。
DI/IOC 优点
DI 与 IOC 一般可以等价之,亦即同一行为的两种说法——若不细究,结果和目的是一样的。上文我们分析如何做到 DI 的,但现在我们回过头来,重新思考下 DI 到底有什么好处?话不多说,看看使用前和使用后的效果对比,见代码:
public class A{
private B b;
public A(B b){ this.b = b; }
public void mymethod(){ b.m(); }
}
//使用前: A a = new A(new B()); a. mymethod(); // 使用后: A a = factory.getA(); a. mymethod();创建 A 对象依赖于 B 对象,于是在 A 构造器中传入了 B 对象,才能执行 mymethod()。——这是没有使用 DI 的写法。引入 DI 后 B 实例话消失了!它被自动创建起来,很神奇吧!?如同施展魔法一般!如果我说得还不够明白,可以看看这仁兄所说的:
上面两种方式重要区别:
前者需要照顾B类中A类的实例化,如果B类中调用不只A类一个,还有更多其他类如C/D/E等类,这样,你在使用B类时,还需要研究其他类的创建,如果C/D/E这些类不是你自己编写,你还需要翻阅它们的API说明,研究它们应该如何创建?是使用New 还是工厂模式 还是单态调用?
这时,你会感叹:哇,有没有搞错?我只不过是为了使用B类中一个小小的方法,就花去我这么多时间和精力?
当我们使用第二种方式时,就无需花很多精力和时间考虑A/C/D/E等类的创建。
使用Ioc容器,你再也不必做这些僵化愚蠢的工作了,我们只需从ioc容器中抓取一个类然后直接使用它们。
简单说避免多次 new 对象,所以想出一个方法可以一次性地创建对象就好了。此阶段,完成了 DI 这一定义的工作,也就是回答了“既然我们可以自动销毁对象了,那么为什么不能自动创建对象呢?”这个问题。
那么我们不妨再深挖一下—— DI 的意义就仅限于此吗?当然不是,DI 还有解耦的优点,那才是 DI 的意义(因为虽然我们不用 new 对象出来,但还是要写相关配置的)。
回看使用 DI 前的例子,new 那么多对象相当于是写死的那个、那个对象。但 DI 不会,相反它开了口子,可以允许开发者扩展。能够扩展的前提是什么?是分层。分得越多,颗粒度越细。下层只要满足上层接口,即可被上层(父类)所使用,而不管你下层有多少个子类,多少层扩展。这样就达到了添加新业务的目的。换句话说这一过程便是“解耦”了。发挥这一作用的背后相当于是一个第三方角色,有了第三方那么配置才变成可能——尽管对使用者而言第三方通常是隐藏的。
DI 对参与的类设计是有要求的,一个是基于“面向接口编程”,另外一个是 Java Bean。先说说接口。注入的目标类要求是通过 接口 定义的。所以强调“面向接口编程”是多么重要。如果扩展新子类(实现了接口)仍不满足,那就要修改 接口了。但原有的实现类虽然要增加方法但是不用给出实现(因为一旦实现了接口就是全局的)。
参考: