本身动手编写IOC框架(三)

  刚写博客浏览量第一天就有1000多人次,给了我很大的鼓舞决定熬夜再写一篇。对于前两篇来讲无非就是使用dtd验证xml,而后解析xml,和IOC的核心仍是差的很远,相信不少小伙伴们都感受看得不过瘾了,这期咱们就进入正题了。java

  先说说上期有个小伙伴提意见让我把IocUtil类使用反射不要用那么多if-else当时以为颇有道理,可是回来仔细想了下,通常数据类型仍是要和其余类型分开否则无法处理,IocUtil代码再次贴上若是有高手以为能够改动,能够再次给我意见,再次谢谢那位给意见的小伙伴。spring

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片断状况下的对象的生成dom

<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>

细心的小伙伴可能发现了一个很总要的问题,那么下面的片断怎么去生成对象呢?ide

<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;
    }

}

详细注释见代码。以上就实现了设值注入的方法。这一期因为时间关系测试代码就不写了。this

有想法的小伙伴们可能就会发现,从第一期的document层提供dtd对xml的绑定校验,到loader层提供将document以dom4j中的Element的形式加载到内存的方法,再到parser层中对内存中的全部Element的针对各类标签的解析方法,再到create层的针对类的全名及构造参数建立类的对象的方法,针对生成的类依据property属性的设值注入的方法。看上去说了这么就彻底就是一个个的工具类而已,最多能够算一个从底下一层层上来的工具而已,对于整个Ioc来讲半点功能都没有实现,这有什么用呢?哈哈这就是本项目设计的巧妙之处,后面只要稍做处理就能化腐朽为神奇,使看起来散乱的工具类一下拼接成完成的框架。小伙伴们也能够趁机多考虑下泪滴会怎么实现呢?若是是大家大家会怎么实现呢?固然要知道我怎么实现的,敬请期待下一期,哈哈,下期再见。屌丝专用百度云代码地址http://pan.baidu.com/s/1gxNwaspa

相关文章
相关标签/搜索