最新完整代码在: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:被注入的类可以不限定某个类了,能够支持父类和接口!详见更新的源码。
参考: