自己动手编写IOC框架(三)
刚写博客浏览量第一天就有1000多人次,给了我很大的鼓舞决定熬夜再写一篇。对于前两篇来说无非就是使用dtd验证xml,然后解析xml,和IOC的核心还是差的很远,相信很多小伙伴们都感觉看得不过瘾了,这期我们就进入正题了。
先说说上期有个小伙伴提意见让我把IocUtil类使用反射不要用那么多if-else当时觉得很有道理,但是回来仔细想了下,一般数据类型还是要和其他类型分开不然没法处理,IocUtil代码再次贴上如果有高手觉得可以改动,可以再次给我意见,再次谢谢那位给意见的小伙伴。
package com.tear.ioc.util; /** * 这是一个帮助类 */ public class IocUtil { /** * 如果该类型是java中的几个基本数据类型那么返回它的类型,注意Integer.type就是获得他的class对象 * 如果不是基础类型则使用getClass()返回它的Class对象 * @param obj * @return */ public static Class<?> getClass(Object obj) { if (obj instanceof Integer) { return Integer.TYPE; } else if (obj instanceof Boolean) { return Boolean.TYPE; } else if (obj instanceof Long) { return Long.TYPE; } else if (obj instanceof Short) { return Short.TYPE; } else if (obj instanceof Double) { return Double.TYPE; } else if (obj instanceof Float) { return Float.TYPE; } else if (obj instanceof Character) { return Character.TYPE; } else if (obj instanceof Byte) { return Byte.TYPE; } return obj.getClass(); }/** * 判断className的类型是否为基础类型。如java.lang.Integer, 是的话将数据进行转换 * 成对应的类型该方法是供本类中的方法调用的,作用是根据type类型的值将对应的value数据转换 * 成对应的type类型的值 * @param className * @param data * @return */ public static Object getValue(String className, String data) { /** * 下面的所有if和else if都是判断是否是java的8中基本数据类型的包装类型 */ if (isType(className, "Integer")) { return Integer.parseInt(data); } else if (isType(className, "Boolean")) { return Boolean.valueOf(data); } else if (isType(className, "Long")) { return Long.valueOf(data); } else if (isType(className, "Short")) { return Short.valueOf(data); } else if (isType(className, "Double")) { return Double.valueOf(data); } else if (isType(className, "Float")) { return Float.valueOf(data); } else if (isType(className, "Character")) { /** * 如果是Character类型则取第一个字符 */ return data.charAt(0); } else if (isType(className, "Byte")) { return Byte.valueOf(data); } else { /** * 如果不是8种基本数据类型的包装类那么就是自定义的类了,直接返回该值 */ return data; } } /** * 该方法是判断类名中是否含有对应的type字符串的方法,如判断className:java.lang.Integer中 * 是否包含Integer这样就返回true,不包含则返回false,该方法是供上面的方法调用的 * @param className * @param type * @return */ private static boolean isType(String className, String type) { if (className.lastIndexOf(type) != -1) return true; return false; } }
前两期已经把将Ioc所需要用到的配置文件xml从dtd验证到加载内存到解析一整套流程介绍完了。这期我们应该处理从xml解析出来的各个bean元素了。由于Ioc的就是给你生成对象,生成对象不管是普通的new还是使用反射,无非就是直接或者间接的使用无参数的构造方法或者是有参数的构造方法,。我们创建一个包com.tear.ioc.bean.create然后在这个包下定义一个生成对象的接口BeanCreator
package com.tear.ioc.bean.create; import java.util.List; /** * 这是一个创建bean的接口 * @author rongdi * */ public interface BeanCreator { /** * 使用无参的构造器创建bean实例, 不设置任何属性 * @param className * @return */ public Object createBeanUseDefaultConstruct(String className); /** * 使用有参数的构造器创建bean实例, 不设置任何属性 * @param className * @param args 参数集合 * @return */ public Object createBeanUseDefineConstruct(String className, List<Object> args); }
上面接口实际上传入的className字符串就是从xml中解析出来的配置的类的全名,至于args就是解析出的所有constructor-arg标签下的值组装出来的集合。关键部分就是看这个接口的实现类了。实现类BeanCreatorImpl如下
package com.tear.ioc.bean.create; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import com.tear.ioc.bean.exception.BeanCreateException; import com.tear.ioc.util.IocUtil; /** * 这是一个使用构造方法创建bean对应的实例的类 * @author rongdi * */ public class BeanCreatorImpl implements BeanCreator { /** * 使用默认的构造方法创建实例,传入的参数为类的全名,可以从bean的class属性的值那里获得 * 再通过反射创建实例 */ @Override public Object createBeanUseDefaultConstruct(String className) { try { /** * 获得类的全名对应的Class对象 */ Class<?> clazz = Class.forName(className); /** * 使用反射的方式返回一个该类的实例,使用的是无参数的构造方法 */ return clazz.newInstance(); } catch (ClassNotFoundException e) { throw new BeanCreateException("没有找到"+className+"该类 " + e.getMessage()); } catch (Exception e) { throw new BeanCreateException(e.getMessage()); } } @Override public Object createBeanUseDefineConstruct(String className, List<Object> args) { /** * 将传入的List<Object>类型的参数转换成Class数组的形式 */ Class<?>[] argsClass = this.getArgsClasses(args); try { /** * 获得传入类的全名的Class对象 */ Class<?> clazz = Class.forName(className); /** * 通过反射得到该类的构造方法(Constructor)对象 */ Constructor<?> constructor = getConstructor(clazz, argsClass); /** * 根据参数动态创建一个该类的实例 */ return constructor.newInstance(args.toArray()); } catch (ClassNotFoundException e) { e.printStackTrace(); throw new BeanCreateException(className+"类没有找到 " + e.getMessage()); } catch (NoSuchMethodException e) { e.printStackTrace(); throw new BeanCreateException("没找到"+className+"中对应的构造方法" + e.getMessage()); } catch (Exception e) { e.printStackTrace(); throw new BeanCreateException(e.getMessage()); } } /** * 根据类的Class对象和参数的Class对象的列表查找一个类的构造器,注意一般我们定义方法的时候 * 由于为了使用多态原理一般我们将方法里的参数定义成我们想接受的参数的一个父类或者是父接口,这样我们 * 想通过该方法就获得不到Constructor对象了,所以该方法只是一个初步的方法还需要进行封装才能 * 达到我们想要的效果 * @param clazz 类型 * @param argsClass 构造参数 * @return */ private Constructor<?> getProcessConstructor(Class<?> clazz, Class<?>[] argsClass) { try { Constructor<?> constructor = clazz.getConstructor(argsClass); return constructor; } catch (NoSuchMethodException e) { return null; } } /** * 这个方法才是真正或得到构造方法的 * @param clazz * @param argsClass * @return * @throws NoSuchMethodException */ private Constructor<?> getConstructor(Class<?> clazz, Class<?>[] argsClass) throws NoSuchMethodException { /** * 首先调用获得直接根据类名和参数的class列表的构造方法,如果该构造方法要传入的类不是构造方法 * 中声明的类,一般需要注入的类都是它的子类,这样该方法获得的构造方法对象就是null的,这样 * 就需要在获得所有的构造方法,判断传入的是否是构造方法形式参数的实例对象了 */ Constructor<?> constructor = getProcessConstructor(clazz, argsClass); /** * 如果获得的构造方法对象为空 */ if (constructor == null) { /** * 得到该类的所有的public的构造器对象 */ Constructor<?>[] constructors = clazz.getConstructors(); /** * 遍历所有的构造器对象 */ for (Constructor<?> c : constructors) { /** * 获取到该构造器的所有参数的class对象的Class数组形式 */ Class<?>[] tempClass = c.getParameterTypes(); /** * 判断该构造器的参数个数是否与argsClass(传进来)的参数个数相同 */ if (tempClass.length == argsClass.length) { if (isSameArgs(argsClass, tempClass)) { return c; } } } } else { /** * 如果传入的刚好是构造器中定义的那个类,就会之金额找到该构造器,那么直接返回该构造器 */ return constructor; } /** * 如果到这里还没返回表示没有找到合适的构造器,直接抛出错误 */ throw new NoSuchMethodException("找不到指定的构造器"); } /** * 判断两个参数数组类型是否匹配 * @param argsClass * @param constructorArgsCLass * @return */ private boolean isSameArgs(Class<?>[] argsClass, Class<?>[] tempClass) { /** * for循环比较每一个参数是否都相同(子类和父类看成相同) */ for (int i = 0; i < argsClass.length; i++) { try { /** * 将传入参数(前面的参数)与构造器参数,后面的参数进行强制转换,如果转换成功说明前面的参数 * 是后面的参数的 子类,那么可以认为类型相同了,若果不是子类就会抛异常 */ argsClass[i].asSubclass(tempClass[i]); /** * 循环到最后一个参数都成功转换表示类型相同,该构造器合适了 */ if (i == (argsClass.length - 1)) { return true; } } catch (Exception e) { /** * 如果有一个参数类型不符合, 跳出该循环 */ break; } } return false; } /** * 得到参数集合的class数组 * @param args * @return */ private Class<?>[] getArgsClasses(List<Object> args) { /** * 定义一个集合保存所需参数 */ List<Class<?>> result = new ArrayList<Class<?>>(); /** * 循环所有传入参数 */ for (Object arg : args) { /** * 将参数转换成对应的Class对象后加到定义的集合中来 */ result.add(IocUtil.getClass(arg)); } /** * 根据该集合的长度创建一个相同长度的Class数组 */ Class<?>[] a = new Class[result.size()]; /** * 返回集合对应的Class数组 */ return result.toArray(a); } }
实现类中那么多的代码基本都是私有的,只是为了完成接口的两个实现类,总的来说实现类的作用主要是用来解决下面xml片段情况下的对象的生成
<bean id="test3" class="com.tear.Test3"></bean> <bean id="test4" class="com.tear.Test4"> <constructor-arg> <value type="java.lang.String">zhangsan</value> </constructor-arg> <constructor-arg> <value type="java.lang.String">123456</value> </constructor-arg> </bean> <bean id="test5" class="com.tear.Test5"> <constructor-arg> <ref bean="test3"/> </constructor-arg> <constructor-arg> <ref bean="test3"/> </constructor-arg> </bean>
细心的小伙伴可能发现了一个很总要的问题,那么下面的片段怎么去生成对象呢?
<bean id="test1" class="com.rongdi.Test1"></bean> <bean id="test2" class="com.rongdi.Test2"></bean> <bean id="test3" class="com.tear.Test3"> <property name="aa"> <ref bean="test1"/> </property> <property name="bb"> <ref bean="test2"/> </property> </bean>
如果说构造方法注入属性叫做构造注入那么这种就是设值注入了,很容易想到的就是先生成一个对象然后再调用该对象的相应的set方法去将参数set进去。为了将接口实现类的方式进行到底我们再次在com.rongdi.ioc.beans.create包下定义一个处理PropertyHandler接口,该接口及其实现类主要是负责完成对一个已有对象进行设值处理。可以很简单的想象,可能就需要一个方法,该方法两个参数一个参数就是需要设值的对象,第二个参数就是该对象需要设置的值,因为值可能有多个,每个值到底设置到哪里,所以我们需要一个map类型的参数;可能还需要一个方法就是获取该需要设置值的对象所在类里面的所有setXX方法。
package com.tear.ioc.bean.create; import java.lang.reflect.Method; import java.util.Map; /** * 处理属性的接口 * @author rongdi * */ public interface PropertyHandler { /** * 为对象obj设置属性,第一个参数是需要设置值的对象,第二个参数是给Object里面的变量 * 设什么值,Map的key就是property元素的name属性对应于对象的成员变量名,Map的value * 就是对应的值 * @param obj * @param properties 属性集合 * @return */ public Object setProperties(Object obj, Map<String, Object> properties); /** * 返回一个对象里面所有的setter方法, 封装成map, key为setter方法名去掉set后的字符串 * 至于为什么是这样的,具体原因在实现类中的私有方法getMethodNameWithOutSet已经做了 * 详细的解释 * @param obj * @return */ public Map<String, Method> getSetterMethodsMap(Object obj); /** * 使用反射执行一个方法,主要是来完成调用一次就为对象设置一个属性 * @param object 需要执行方法的对象 * @param argBean 参数的bean * @param method setXX方法对象 */ public void executeMethod(Object object, Object argBean, Method method); }
具体实现类PropertyHandlerImpl如下
package com.tear.ioc.bean.create; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import com.tear.ioc.bean.exception.BeanCreateException; import com.tear.ioc.bean.exception.PropertyException; import com.tear.ioc.util.IocUtil; /** * 这是处理属性的类 * @author rongdi * */ public class PropertyHandlerImpl implements PropertyHandler { /** * 为对象obj设置属性,第一个参数是需要设置值的对象,第二个参数是给Object里面的变量 * 设什么值,Map的key就是property元素的name属性对应于对象的成员变量名,Map的value * 就是对应的值 */ @Override public Object setProperties(Object obj, Map<String, Object> properties) { /** * 得到需要设置的对象obj的Class对象 */ Class<?> clazz = obj.getClass(); try { /** * 遍历Map中所有的key值,该key值就是对象中需要使用setXXX方法设值的成员变量 */ for (String key : properties.keySet()) { /** * 调用本类中定义的getSetterMethodName方法获得一个属性的成员变量 * 对应的set方法 */ String setterName = this.getSetterMethodName(key); /** * 获得要给该成员变量设置的值的Class对象 */ Class<?> argClass = IocUtil.getClass(properties.get(key)); /** * 通过反射找到obj对象对应的setXXX方法的Method对象 */ Method setterMethod = getSetterMethod(clazz, setterName, argClass); /** * 通过反射调用该setXXX方法,传入Map中保存的对应的值 */ setterMethod.invoke(obj, properties.get(key)); } return obj; } catch (NoSuchMethodException e) { throw new PropertyException("对应的setter方法没找到" + e.getMessage()); } catch (IllegalArgumentException e) { throw new PropertyException("wrong argument " + e.getMessage()); } catch (Exception e) { throw new PropertyException(e.getMessage()); } } /** * 返回一个属性的setter方法 * @param propertyName * @return */ private String getSetterMethodName(String propertyName) { return "set" + this.firstWordToUpperCase(propertyName); } /** * 将参数s的首字母变为大写 * @param key * @return */ private String firstWordToUpperCase(String s) { String firstWord = s.substring(0, 1); String upperCaseWord = firstWord.toUpperCase(); return s.replaceFirst(firstWord, upperCaseWord); } /** * 通过反射得到methodName对应的Method对象,第一个参数为操作对象的Class对象 * 第二个参数为需要操作的方法名,第三个是需要操作的方法的参数的Class列表 * @param objClass * @param methodName * @param argClass * @return * @throws NoSuchMethodException */ private Method getSetterMethod(Class<?> objClass, String methodName, Class<?> argClass) throws NoSuchMethodException { /** * 使用原类型获得方法,也就是不算它的父类或者是父接口。 如果没有找到该方法, 则得到null */ Method argClassMethod = this.getMethod(objClass, methodName, argClass); /** * 如果找不到原类型的方法, 则找该类型所实现的接口 */ if (argClassMethod == null) { /** * 调用本类定义的getMethods方法得到所有名字为methodName的并且只有一个参数的方法 */ List<Method> methods = this.getMethods(objClass, methodName); /** * 调用本类定义的findMethod方法找到所需要的Method对象 */ Method method = this.findMethod(argClass, methods); if (method == null) { /** * 找不到任何方法直接抛异常 */ throw new NoSuchMethodException(methodName); } /** * 方法不为空说明找到方法,返回该方法对象 */ return method; } else { /** * 找到了原参数类型的方法直接返回 */ return argClassMethod; } } /** * 根据方法名和参数类型得到方法, 如果没有该方法返回null * @param objClass * @param methodName * @param argClass * @return */ private Method getMethod(Class<?> objClass, String methodName, Class<?> argClass) { try { Method method = objClass.getMethod(methodName, argClass); return method; } catch (NoSuchMethodException e) { return null; } } /** * 得到所有名字为methodName并且只有一个参数的方法 * @param objClass * @param methodName * @return */ private List<Method> getMethods(Class<?> objClass, String methodName) { /** * 创建一个ArrayList集合用来保存所需要的Method对象 */ List<Method> result = new ArrayList<Method>(); /** * 通过反射得到所有的方法后遍历所有的方法 */ for (Method m : objClass.getMethods()) { /** * 如果方法名相同 */ if (m.getName().equals(methodName)) { /** * 得到方法的所有参数, 如果只有一个参数, 则添加到集合中 */ Class<?>[] c = m.getParameterTypes(); /** * 如果只有一个参数就加到ArrayList中 */ if (c.length == 1) { result.add(m); } } } /** * 返回所需要的集合 */ return result; } /** * 方法集合中寻找参数类型是interfaces其中一个的方法 * @param argClass 参数类型 * @param methods 方法集合 * @return */ private Method findMethod(Class<?> argClass, List<Method> methods) { /** * 遍历所有找到的方法 */ for (Method m : methods) { /** * 判断参数类型与方法的参数类型是否一致,如果一致说明找到了对应的方法。返回该方法 */ if (this.isMethodArgs(m, argClass)) { return m; } } /** * 没找到该方法返回null */ return null; } /** * 得到obj对象中的所有setXXX方法的Map映射,Map的key值为对应的属性名,value为对应的set * 方法的Method对象 */ public Map<String, Method> getSetterMethodsMap(Object obj) { /** * 调用本类中所定义的getSetterMethodsList方法得到所有的setXXX方法 */ List<Method> methods = this.getSetterMethodsList(obj); /** * 定义一个结果的映射用来存放方法的属性名(对应到bean里面就是bean的id属性的值) * 与对应的set方法 */ Map<String, Method> result = new HashMap<String, Method>(); /** * 遍历所有的Method对象,调用本类的getMethodNameWithoutSet得到该方法 * 对应的属性名(也就是去掉set后的值) */ for (Method m : methods) { String propertyName = this.getMethodNameWithOutSet(m.getName()); /** * 将所需的属性名和方法对信息放入map中 */ result.put(propertyName, m); } /** * 返回所需的map */ return result; } /** * 将setter方法还原, setName作为参数, 得到name * @param methodName * @return */ private String getMethodNameWithOutSet(String methodName) { /** * 得到方法名中去掉set之后的名字,为什么是这样的,我们不得不说下在设值注入的时候实际上 * 到底注入什么参数实际上是看setXxx方法去掉set然后再把后面的第一个字符变成小写之后的xxx * 作为依据如一个类中有属性 * private String yy; * public void setXx(String aa) { * this.yy = aa; * } * <bean id="test3" class="com.tear.Test3"> * <property name="xx"> * <value type="java.lang.String">123456</value> * </property> * </bean> * 实际上这里的property标签的name值要注入的地方的依据并不是去找类中属性名为xx的去设置 * 而是去找xx的第一个字符大写前面加上set即setXx方法来完成设值,所以看到xx属性实际上会 * 注入到了yy成员变量中,所以这里的配置文件的属性的值的注入始终是找该属性变成相应的set方法 * 去设值的,这一点不管在struts2的action层还是在spring的Ioc都有很明显的表现,不相信的 * 小伙伴可以自己去试试。当然作为自己的实现你可以自己定义设值的规则 */ String propertyName = methodName.substring(3); /** * 得到该属性名的第一个大写字母 */ String firstWord = propertyName.substring(0, 1); /** * 将大写字母换成小写的 */ String lowerFirstWord = firstWord.toLowerCase(); /** * 返回该setXXX方法对应的正确的属性名 */ return propertyName.replaceFirst(firstWord, lowerFirstWord); } /** * 通过反射得到obj对象中的所有setXXX方法对应的Method对象的集合形式 * @param obj * @return */ private List<Method> getSetterMethodsList(Object obj) { /** * 反射的入口,首先得到obj对象的Class对象 */ Class<?> clazz = obj.getClass(); /** * 由该对象的Class对象得打所有的方法 */ Method[] methods = clazz.getMethods(); /** * 声明一个结果集合,准备用来方法所需要的Method对象 */ List<Method> result = new ArrayList<Method>(); /** * 遍历所有得到的Method对象找到set开头的方法将其放到结果集合中 */ for (Method m : methods) { if (m.getName().startsWith("set")) { result.add(m); } } /** * 返回所需要的Method对象的结果集合 */ return result; } /** * 执行某一个方法,该方法用来被自动装配的时候调用对应对象中的setter方法把产生的argBean对象set进去的方法 * 其中object为带设值的对象,argBean就是要设进去的参数,第三个参数就是setXX对象本身的Method对象,主要是 * 方便使用反射 */ public void executeMethod(Object object, Object argBean, Method method) { try { /** * 获取需要调用的方法的参数类型 */ Class<?>[] parameterTypes = method.getParameterTypes(); /** * 如果参数数量为1,则执行该方法,因为作为setXX方法参数个数肯定 * 是1 */ if (parameterTypes.length == 1) { /** * 如果参数类型一样, 才执行方法 */ if (isMethodArgs(method, parameterTypes[0])) { method.invoke(object, argBean); } } } catch (Exception e) { /** * 因为如该方法主要是被自动装备的方法调用,所以如果遇到问题抛出自动装配异常的信息 */ throw new BeanCreateException("自动装配异常 " + e.getMessage()); } } /** * 判断参数类型(argClass)是否是该方法(m)的参数类型 * @param m * @param argClass * @return */ private boolean isMethodArgs(Method m, Class<?> argClass) { /** * 得到方法的参数类型 */ Class<?>[] c = m.getParameterTypes(); /** * 如果只有一个参数才符合要求 */ if (c.length == 1) { try { /** * 将参数类型(argClass)与方法中的参数类型进行强制转换, 不抛异常说明 * 传入的参数是方法参数的子类的类型,或者就是方法参数的类型。 */ argClass.asSubclass(c[0]); /** * 没抛异常返回true,其他情返回false */ return true; } catch (ClassCastException e) { return false; } } return false; } }
详细注释见代码。以上就实现了设值注入的方法。这一期由于时间关系测试代码就不写了。
有想法的小伙伴们可能就会发现,从第一期的document层提供dtd对xml的绑定校验,到loader层提供将document以dom4j中的Element的形式加载到内存的方法,再到parser层中对内存中的所有Element的针对各种标签的解析方法,再到create层的针对类的全名及构造参数创建类的对象的方法,针对生成的类依据property属性的设值注入的方法。看上去说了这么就完全就是一个个的工具类而已,最多可以算一个从底下一层层上来的工具而已,对于整个Ioc来说半点功能都没有实现,这有什么用呢?哈哈这就是本项目设计的巧妙之处,后面只要稍作处理就能化腐朽为神奇,使看起来散乱的工具类一下拼接成完成的框架。小伙伴们也可以趁机多考虑下泪滴会怎么实现呢?如果是你们你们会怎么实现呢?当然要知道我怎么实现的,敬请期待下一期,哈哈,下期再见。屌丝专用百度云代码地址http://pan.baidu.com/s/1gxNwa