极简版 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.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:被注入的类可以不限定某个类了,能够支持父类和接口!详见更新的源码。

参考:

  • 《Ioc容器的革命性优点》 此文对 ioc 概念讲解得比较透彻,大意是为避免多次 new 对象,所以想出一个方法可以一次性地创建对象就好了——不错这样会很爽,但是问题来了,各自依赖的对象,怎么配置?(许多情况下,至少要有两个类才能联合起来完成任务。我们如果不能选择继承,就通常是委托。但如何优雅地委托呢?Spring 赋予了 委托 新的诠释。既然是语言层次的增强,那么只要在 Java 应用到的场合,都可以用 Spring 的 IOC 哦。)
  • XML 版本:《菜鸟学SSH(十三)——Spring容器IOC解析及简单实现》

你可能感兴趣的:(极简版 Java 依赖注射)