自己动手编写IOC框架(四)

  终于到了激动人心的时刻了,首先感谢小伙伴们的阅读,如果能多点评论,多点探讨就更好了,没有交流让我觉得我写的东西只有标题有点价值,内容只是在浪费大家的时间。为了泪滴下周能写下一个框架orm,请小伙伴们能给点信心。前面3篇中介绍的大都是完成某一个层面的工具式的类,看起来就像是一盘散沙。原因就是缺少一个能够统管这盘散沙的头头,那么这篇内容将会以一个头头的角度告诉大家什么才叫化腐朽为神奇。

  我们先回想下spring框架中是否会出现如下类似的代码呢?

ApplicationContext context = new FileSystemXmlApplicationContext(filePath);

这个代码就是根据xml文件的路径获取了spring中的context对象,我习惯叫做上下文对象,根据这个对象我们就可以通过调用他的getBean方法根据id来获取到我们需要的实例对象了。下面我们也给我们的框架来一个类似的对象,当然之前我们仍然应该将接口实现类的模式进行到底。接口放在新建的包com.tear.ioc.context下面

package com.tear.ioc.context;

/**
 * 这是ioc应用容器的接口
 * @author rongdi
 *
 */
public interface ApplicationContext {
    /**
     * 根据id找到bean对应的对象的实例
     * @param id
     * @return
     */
    public Object getBeanInstance(String id);
    /**
     * IoC容器中是否包含id为参数的bean
     * @param id
     * @return
     */
    public boolean beanIsExist(String id);
    
    /**
     * 判断一个bean是否为单态
     * @param name
     * @return
     */
    public boolean isSingleton(String id);
    
    /**
     * 从容器中获得bean对应的实例, 如果从容器中找不到该bean, 返回null
     * @param id
     * @return
     */
    public Object getBeanWithoutCreate(String id);
}

为了使用方便我们再给他一个抽象类的实现AbstractApplicationContext

package com.tear.ioc.context;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.dom4j.Document;
import org.dom4j.Element;

import com.tear.ioc.bean.create.BeanCreator;
import com.tear.ioc.bean.create.BeanCreatorImpl;
import com.tear.ioc.bean.create.PropertyHandler;
import com.tear.ioc.bean.create.PropertyHandlerImpl;
import com.tear.ioc.bean.exception.BeanCreateException;
import com.tear.ioc.bean.xml.autowire.Autowire;
import com.tear.ioc.bean.xml.autowire.ByNameAutowire;
import com.tear.ioc.bean.xml.autowire.NoAutowire;
import com.tear.ioc.bean.xml.document.DocumentHolder;
import com.tear.ioc.bean.xml.document.XmlDocumentHolder;
import com.tear.ioc.bean.xml.element.CollectionElement;
import com.tear.ioc.bean.xml.element.LeafElement;
import com.tear.ioc.bean.xml.element.PropertyElement;
import com.tear.ioc.bean.xml.element.RefElement;
import com.tear.ioc.bean.xml.element.ValueElement;
import com.tear.ioc.bean.xml.element.loader.ElementLoader;
import com.tear.ioc.bean.xml.element.loader.ElementLoaderImpl;
import com.tear.ioc.bean.xml.element.parser.BeanElementParser;
import com.tear.ioc.bean.xml.element.parser.BeanElementParserImpl;
/**
 * 该类继承ApplicationContext接口,定义成抽象类是因为本类中定义的方法还不够完善
 * 不想本类被直接实例化来使用希望使用它扩展功能后的子类
 * @author rongdi
 *
 */
public abstract class AbstractApplicationContext implements ApplicationContext {

    /**
     * 定义一个文档持有对象
     */
    protected DocumentHolder documentHolder = new XmlDocumentHolder();
    /**
     * 定义一个元素加载对象
     */
    protected ElementLoader elementLoader = new ElementLoaderImpl();
    /**
     * 定义一个Element元素读取类
     */
    protected BeanElementParser elementParser = new BeanElementParserImpl();
    /**
     * 定义一个创建bean对象的类
     */
    protected BeanCreator beanCreator = new BeanCreatorImpl();
    /**
     * 定义一个属性处理类
     */
    protected PropertyHandler propertyHandler = new PropertyHandlerImpl();
    /**
     * 定义一个Map用来保存bean元素的id和生成的对应的实例,主要是单实例的bean的对象需要保存起来
     */
    protected Map<String, Object> beanInstances = new HashMap<String, Object>();
    
    /**
     * 初始化Elements将对应的document元素中的Element调用elementLoader的方法缓存起来
     * 可以读取多个xnl文件的路径,参数为一个字符串数组
     * @param xmlPaths
     */
    protected void initElements(String[] xmlPaths) {
        try {            
            /**
             * 获取当前项目的根路径
             */
            URL classPathUrl = AbstractApplicationContext.class.getClassLoader().getResource(".");
            /**
             * 为防止路径出现汉字乱码的情况使用utf-8进行解码
             */
            String classPath = java.net.URLDecoder.decode(classPathUrl.getPath(),"utf-8");
            /**
             * 遍历所有的路径
             */
            for (String path : xmlPaths) {
                /**
                 * 由根路径加传入的相对路径获取document元素
                 */
                Document doc = documentHolder.getDocument(classPath + path);
                /**
                 * 将所有的路径对应的xml文件里的bean元素都缓存到elementLoader对象中
                 */
                elementLoader.addBeanElements(doc);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
    /**
     * 创建一个bean实例, 如果找不到该bean对应的配置文件的Element对象, 抛出异常
     * @param id
     * @return
     */
    protected Object createBeanInstance(String id) {
        /**
         * 首先直接在elementLoader对象中找id对应的元素
         */
        Element e = elementLoader.getBeanElement(id);
        /**
         * 如果没找到抛出异常
         */
        if (e == null) 
            throw new BeanCreateException("没找到 " + id+"对应的bean元素");
        /**
         * 调用本类定义的instance方法实例化该元素对应的类
         */
        Object result = this.instanceBeanElement(e);
        System.out.println("创建bean: " + id);
        System.out.println("该bean的对象是: " + result);
        /**
         * 设值注入, 先判断是否自动装配
         */
        Autowire autowire = elementParser.getAutowire(e);
        if (autowire instanceof ByNameAutowire) {
            /**
             * 使用名称自动装配
             */
            autowireByName(result);
        } else if (autowire instanceof NoAutowire) {
            /**
             * 调用设置注入,给根据e元素生成的对象result设置e元素里配置的property属性值
             */
            this.setterInject(result, e);
        }
        /**
         * 返回创建的实例result
         */
        return result;
    }
    /**
     * 实例化一个bean, 如果该bean的配置有constructor-arg元素, 那么使用带参数的构造器
     * @param e
     * @return
     */
    protected Object instanceBeanElement(Element e) {
        /**
         * 得到该bean元素的class属性的值
         */
        String className = elementParser.getAttribute(e, "class");
        /**
         * 得到bean节点下面的constructor-arg元素
         */
        List<Element> constructorElements = elementParser.getConstructorArgsElements(e);
        /**
         * 判断使用什么构造器进行创建(判断标准为bean元素下是否有constructor-arg子元素)
         * 如果没有constructor-arg子元素调用无参构造器
         */
        if (constructorElements.size() == 0) {
            /**
             * 没有constructor-arg子元素, 使用无参构造器
             */
            return beanCreator.createBeanUseDefaultConstruct(className);
        } else {
            /**
             * 有constructor-arg子元素,得到所有的构造参数 使用有参数构造器, 构造注入参数
             */
            List<Object> args = getConstructArgs(e);
            return beanCreator.createBeanUseDefineConstruct(className, args);
        }
    }
    /**
     * 创建所有的bean的实例, 延迟加载的不创建
     */
    protected void createBeanInstances() {
        /**
         * 获取保存到elementLoader对象中的Bean元素
         */
        Collection<Element> elements = elementLoader.getBeanElements();
        /**
         * 遍历所有的bean元素
         */
        for (Element e : elements) {
            /**
             * 得到bean元素的lazy属性值
             */
            boolean lazy = elementParser.isLazy(e);
            /**
             * 如果不是延迟加载
             */
            if (!lazy) {
                /**
                 * 得到该元素的id属性值
                 */
                String id = e.attributeValue("id");
                /**
                 * 创建这个id所对应的bean的实例
                 */
                Object bean = this.getBeanInstance(id);
                /**
                 * 如果bean实例进入处理bean的方法中
                 */
                if (bean == null) {
                    /**
                     * 处里bean的方法分是否是单例两种情况进行考虑
                     */
                    handleBean(id);
                }
            }
        }
    }
    
    /**
     * 处理bean, 如果是单态的, 则加到map中, 非单态, 则创建返回
     * @param id
     * @return
     */
    protected Object handleBean(String id) {
        /**
         * 首先根据传入的id属性值找到该bean创建一个对应的bean的实例
         */
        Object beanInstance = createBeanInstance(id);;
        /**
         * 如果是单例的则保存到Map中方便需要的时候取出
         */
        if (isSingleton(id)) {
            /**
             * 单态的话, 放到map中
             */
            this.beanInstances.put(id, beanInstance);
        }
        /**
         * 返回创建的bean的实例
         */
        return beanInstance ;
    }
    
    /**
     * 判断id值对应的bean是否为单态的
     */
    public boolean isSingleton(String id) {
        /**
         * 使用ElementLoader方法获得对应的Element
         */
        Element e = elementLoader.getBeanElement(id);
        /**
         * 使用ElementReader判断是否为单态
         */
        return elementParser.isSingleton(e);
    }

        
    /**
     * 通过property元素为参数obj设置属性
     * @param obj
     * @param e
     */
    protected void setterInject(Object obj, Element beanElement) {
        /**
         * 返回bean元素的所有的property标签对应的元素
         */
        List<PropertyElement> properties = elementParser.getPropertyValue(beanElement);
        /**
         * 调用本类定义的方法得到所需要的属性名与所要设置的值的对信息
         */
        Map<String, Object> propertiesMap = this.getPropertyArgs(properties);
        /**
         * 将对应的值设置到obj对象中
         */
        propertyHandler.setProperties(obj, propertiesMap);
    }
    
    /**
     * 以map的形式得到需要注入的参数对象, key为setter方法对应的属性名, value为参数对象
     * @param properties
     * @return
     */
    protected Map<String, Object> getPropertyArgs(List<PropertyElement> properties) {
        /**
         * 定义一个结果映射保存所需要的属性名与所要设置的值的对信息
         */
        Map<String, Object> result = new HashMap<String, Object>();
        /**
         * 遍历所有的property元素
         */
        for (PropertyElement p : properties) {
            /**
             * 得到prperty元素中的子元素
             */
            LeafElement le = p.getLeafElement();
            /**
             * 判断如果是RefElement元素
             */
            if (le instanceof RefElement) {
                /**
                 * 将对应的属性名和需要设置进去的实例对象保存在map中
                 */
                result.put(p.getName(), this.getBeanInstance((String)le.getValue()));
            } else if (le instanceof ValueElement) {
                /**
                 * 如果是ValueElement,将对应的属性名和需要设置的值保存到Map中
                 */
                result.put(p.getName(), le.getValue());
            } else if(le instanceof CollectionElement) {
                /**
                 * 先判断是否是CollectionElement如果是再判断Collection标签里面放的是value标签还是ref标签
                 * 可以直接取出Collection里面的List判断还要判断类型是list还是set,根据不同情况调用不同的方法
                 * 将值放入result的map中
                 */
                if(this.childIsValueElement((CollectionElement)le)) {
                    if("list".equals(le.getType())) 
                        result.put(p.getName(),this.arrayToArrayList((Object[])le.getValue()));
                    else {
                        result.put(p.getName(),this.arrayToHashSet((Object[])le.getValue()));
                    }
                }
                else {
                    if("list".equals(le.getType())){
                        result.put(p.getName(),this.arrayToArrayList(this.getValuesIfChildIsRefElement(le)));
                    }
                    else {
                        result.put(p.getName(),this.arrayToHashSet(this.getValuesIfChildIsRefElement(le)));
                    }
                        
                }
            }
        }
        /**
         * 返回该结果信息
         */
        return result;
    }
    /**
     * 如果collectionElement下是ref元素那么调用该方法将集合标签中所有ref标签下对应的实例都生成好后
     * 返回这些实例的一个Object数组形式
     * @param le
     * @return
     */
    protected Object[] getValuesIfChildIsRefElement(LeafElement le) {
        /**
         * 定义一个临时存放的ArrayList
         */
        List<Object> tempList = new ArrayList<Object>();
        /**
         * 遍历在CollectionElement里面取出的Object数组,因为这里都是ref标签,根据对应的值得到实例对象
         */
        for(Object o:(Object[])le.getValue()) {
            tempList.add(this.getBeanInstance((String)o));
        }
        return tempList.toArray();
    }
    
    /**
     * 将数组转换为ArrayList的方法
     * @param obj
     * @return
     */
    protected List<Object> arrayToArrayList(Object[] obj) {
        List<Object> temp = new ArrayList<Object>();
        for(Object o:obj) {
            temp.add(o);
        }
        return temp;
    }
    /**
     * 将数组转换成HashSet的方法
     */
    protected Set<Object> arrayToHashSet(Object[] obj) {
        Set<Object> temp = new HashSet<Object>();
        for(Object o:obj) {
            temp.add(o);
        }
        return temp;
    }
    /**
     * 判断CollectionElement中配置的是ValueElement元素,如果是则返回true
     */
    protected boolean childIsValueElement(CollectionElement ce) {
        /**
         * 在CollectionElement元素中得到保存子元素的list判断该list中是否是ValueElement元素
         * 如果是返回true
         */
        if(ce.getList().get(0) instanceof ValueElement) {
            return true;
        }
        return false;
    }
    /**
     * 得到一个bean里面配置的构造参数
     * @param e
     * @return
     */
    protected List<Object> getConstructArgs(Element beanElment) {
        /**
         * 得到该bean元素所有的构造参数元素,该参数可能使RefElement和ValueElement
         */
        List<LeafElement> datas = elementParser.getConstructorValue(beanElment);
        /**
         * 定义一个结果信息保存得到的的参数集合
         */
        List<Object> result = new ArrayList<Object>();
        /**
         * 遍历所有的构造参数元素
         */
        for (LeafElement d : datas) {
            /**
             * 如果是ValueElement元素那么直接将该元素的值保存到结果中
             */
            if (d instanceof ValueElement) {
                d = (ValueElement)d;
                result.add(d.getValue());
            } else if (d instanceof RefElement) {
                d = (RefElement)d;
                String refId = (String)d.getValue();
                /**
                 * 如果是引用元素, 则直接调getBean去获取(获取不到则创建),本方法本来就间接的被
                 * getgetBeanInstance方法调用了,现在在这个方法里在调用getBeanInstance
                 * 这个方法相当于形成了一个递归调用,递归的出口在引用所对一个的bean中再没有引用
                 */
                result.add(this.getBeanInstance(refId));
            }
        }
        return result;
    }
    
    /**
     * 自动装配一个对象, 得到该bean的所有setter方法, 再从容器中查找对应的bean
     * 例如, 如果bean中有一个setSchool(School)方法, 那么就去查名字为school的bean, 
     * 再调用setSchool方法设入对象中
     * @param obj
     */
    protected void autowireByName(Object obj) {
        /**
         * 得到该对象所有的setXXX方法和对应的属性(bean的id值)
         */
        Map<String, Method> methods = propertyHandler.getSetterMethodsMap(obj);
        /**
         * 遍历所有的方法
         */
        for (String s : methods.keySet()) {
            /**
             * 得到对应的bean元素
             */
            Element e = elementLoader.getBeanElement(s);
            /**
             * 没有对应的元素配置, 继续循环
             */
            if (e == null) continue;
            /**
             * 调用getBeanInstance方法返回beanInstance
             */
            Object beanInstance = this.getBeanInstance(s);
            /**
             * 得到该对象的setter方法
             */
            Method method = methods.get(s);
            /**
             * 将产生的实例调用executeMethod使用反射的方式设值到obj对象中,实现按名字的自动装配
             */
            propertyHandler.executeMethod(obj, beanInstance, method);
            System.out.println("执行"+method.getName()+"方法给对象:"+obj+"注入"+beanInstance);
        }
    }
    /**
     * 得到对应id的bean的实例
     */
    public Object getBeanInstance(String id) {
        Object beanInstance = this.beanInstances.get(id);
        /**
         * 如果获取不到该bean, 则调用handleBean处理
         */
        if (beanInstance == null) {
            /**
             * 判断处理单态或者非单态的bean
             */
            beanInstance = handleBean(id);
        }
        /**
         * 返回得到的bean的实例
         */
        return beanInstance;
    }
    /**
     * 判断对应id的bean元素是否存在(直接到配置文件中找不要到elementLoader对象的缓存中找)
     */
    public boolean beanIsExist(String id) {
        /**
         * 调用ElementLoader对象, 根据id得到对应的Element对象
         */
        Element e = elementLoader.getBeanElement(id);
        return (e == null) ? false : true;
    }

    /**
     * 在本类的缓存中获得id对应的实例,若果该id对应的bean是单态的就能获取到
     */
    public Object getBeanWithoutCreate(String id) {
        return this.beanInstances.get(id);
    }
}

看到上面这个类是否有人会有点感觉了,没错这就是简单的使用了一下外观模式将之前看似无关的类全部加到这个外观类之中,降低了被加入类之间的耦合度,这就是为什么之前看起来那些类看起来没多大关系,就像各自层里面的工具类的原因。由于有了这个外观类的出现就像一个头头统筹了各类资源进行统一的调配。小伙伴们今后也可以多多用用这个模式,用起来也是很简单,呵呵。至于详细的注释我也是写的很辛苦了,全在代码里面了,多琢磨一下就不难理解了。

  到了这个层面了,应该做一下收尾了,可能有很多人会奇怪,我靠,你加了这么个抽象类,我到底要怎么使用才能达到spring的那种根据上下文context获取需要的bean的功能呢?别急,马上就来了。XmlApplicationContext类如下

package com.tear.ioc.context;


/**
 * 这是真正使用的IoC的应用框架类,可以创建所有配置好的类的实例
 * @author Administrator
 *
 */
public class XmlApplicationContext extends AbstractApplicationContext {
        
    
    public XmlApplicationContext(String[] xmlPaths) {
        /**
         * 初始化文档和元素
         */
        initElements(xmlPaths);
        /**
         * 创建所有bean的实例
         */
        createBeanInstances();
    }
}

  哈哈,看了上面的代码是不是很兴奋,Ioc终于完成了,而且只需要调用这个构造方法,将所有的xml文件以数组的形式传入进去就会自动创建所有非延迟加载的bean,而且也会完成依赖注入了。话不多说直接上测试代码就知道怎么用了。测试包test下建立com.tear.ioc.context包和com.tear.ioc.object包,object包下准备了各类的测试bean的类,具体泪有点多不贴出来了,可以直接到后面提供的百度云中去下载。测试用例如下

package com.tear.ioc.context;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.HashSet;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.tear.ioc.object.XmlApplicationContextObject1;
import com.tear.ioc.object.XmlApplicationContextObject2;
import com.tear.ioc.object.XmlApplicationContextObject3;
import com.tear.ioc.object.XmlApplicationContextObject4;

public class XmlApplicationContextTest {
    ApplicationContext ctx;

    @Before
    public void setUp() throws Exception {
        ctx = new XmlApplicationContext(
                new String[] { "/resources/context/XmlApplicationContext1.xml" });
    }

    @After
    public void tearDown() throws Exception {
        ctx = null;
    }

    @Test
    public void testGetBean() {

        // 拿到第一个, 使用无参构造器创建
        XmlApplicationContextObject1 obj1 = (XmlApplicationContextObject1) ctx
                .getBeanInstance("test1");
        assertNotNull(obj1);
    }

    @Test
    public void testSingleton() {
        // test1是单态bean
        XmlApplicationContextObject1 obj1 = (XmlApplicationContextObject1) ctx
                .getBeanInstance("test1");
        XmlApplicationContextObject1 obj2 = (XmlApplicationContextObject1) ctx
                .getBeanInstance("test1");
        assertEquals(obj1, obj2);
        // test3不是单态bean
        XmlApplicationContextObject1 obj3 = (XmlApplicationContextObject1) ctx
                .getBeanInstance("test3");
        XmlApplicationContextObject1 obj4 = (XmlApplicationContextObject1) ctx
                .getBeanInstance("test3");
        assertFalse(obj3.equals(obj4));
    }

    @Test
    public void testConstructInjection() {
        XmlApplicationContextObject1 obj1 = (XmlApplicationContextObject1) ctx
                .getBeanInstance("test1");
        // 拿到第二个, 使用多参数构造器创建
        XmlApplicationContextObject2 obj2 = (XmlApplicationContextObject2) ctx
                .getBeanInstance("test2");
        assertNotNull(obj2);
        assertEquals(obj2.getName(), "rongdi");
        assertEquals(obj2.getAge(), 22);
        assertEquals(obj2.getObject1(), obj1);
    }

    /*
     * 测试自动装配
     */
    @Test
    public void testAutowire() {

        XmlApplicationContextObject3 obj1 = (XmlApplicationContextObject3) ctx
                .getBeanInstance("test4");
        assertNotNull(obj1);
        XmlApplicationContextObject1 obj2 = obj1.getObject1();
        System.out.println(obj2);
        assertNotNull(obj2);
        XmlApplicationContextObject1 obj3 = (XmlApplicationContextObject1) ctx
                .getBeanInstance("object1");
        assertEquals(obj2, obj3);
    }

    /*
     * 测试是否包含该bean
     */
    @Test
    public void testContainsBean() {
        boolean result = ctx.beanIsExist("test1");
        assertTrue(result);
        result = ctx.beanIsExist("test5");
        assertTrue(result);
        result = ctx.beanIsExist("No exists");
        assertFalse(result);
    }

    /*
     * 测试延迟加载
     */
    @Test
    public void testLazyInit() {
        // test5是延迟加载的, 没有调用过getBean方法, 那么容器中就不会创建这个bean
        Object obj = ctx.getBeanWithoutCreate("test5");
        assertNull(obj);
        // System.out.println(obj);
        obj = ctx.getBeanInstance("test5");
        assertNotNull(obj);
        System.out.println(obj);
    }

    /*
     * 测试设值注入
     */
    @Test
    public void testSetProperties() {
        XmlApplicationContextObject3 obj1 = (XmlApplicationContextObject3) ctx
                .getBeanInstance("test6");
        XmlApplicationContextObject1 obj2 = (XmlApplicationContextObject1) ctx
                .getBeanInstance("object1");
        assertEquals(obj1.getName(), "rongdi");
        assertEquals(obj1.getAge(), 22);
        assertEquals(obj1.getObject1(), obj2);
        XmlApplicationContextObject4 obj4 = (XmlApplicationContextObject4) ctx
                .getBeanInstance("test7");
        System.out.println((ArrayList<Object>) obj4.getList());
        System.out.println((HashSet<Object>) obj4.getSet());
        System.out.println((ArrayList<Object>) obj4.getRefTest());
    }

}

从上面测试用例的方法中我们就可以看到我们的Ioc框架的完整使用方法了。相信小伙伴小心琢磨下,人人都能写出自己的Ioc了,当然该Ioc框架功能还不是很完善,,比如spring的Ioc中可以按照类型注入和名称注入,我们只实现了按名称注入,小伙伴们需要自己实现一个AutoWire接口改一下代码不难实现。至于最近一直都很流行的注解配置的方式,我给小伙伴们提一个自己的实现思路,相信大多数人都能实现。注解方式和xml配置方式的区别之处只在于loader层和parser层,我们可以仿照目前的流程写一个注解的loader层和一个注解的parser层然后在我们AbstractApplicationContext类中再集成进入这两个类,然后改下AbstractApplicationContext中的方法就能实现,这也算是外观模式的一个好处了。如果说小伙伴们的呼声比较高,我会考虑在这周抽时间写下注解的实现部分,不然我就认为大家都能自己完成。就会考虑去写下一个框架orm了。再次重申,泪滴分享的项目并非是要取代现存的已经经过时间检验过的完善的框架,只为了让大家更加深入的理解和使用现有的项目,所以代码在性能方面没有花时间作过多的考虑。

  好了,一如既往的屌丝专用百度云源码地址http://pan.baidu.com/s/1bn70g2N

 

posted on 2014-11-26 08:50  码小D  阅读(2511)  评论(9编辑  收藏  举报