极简版 Java 依赖注射

来源:互联网 发布:网络写手如何赚钱 编辑:程序博客网 时间:2024/04/30 09:18

最新完整代码在: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) {// 获取此包的目录 建立一个FileFile 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),乃分析依赖关系并储存之,结构亦十分简单,主要一下组成:

// 存放beanprivate 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。先说说接口。注入的目标类要求是通过 接口 定义的。所以强调“面向接口编程”是多么重要。如果扩展新子类(实现了接口)仍不满足,那就要修改 接口了。但原有的实现类虽然要增加方法但是不用给出实现(因为一旦实现了接口就是全局的)。

参考:

  • 《Ioc容器的革命性优点》
  • XML 版本:《菜鸟学SSH(十三)——Spring容器IOC解析及简单实现》
1 0