极简版 Java 依赖注射

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

参考:

上一篇:详解函数介绍,定义和调⽤ | 手把手教你入门Python之三十九


下一篇:手把手教你十五分钟搭建个人博客网站