.NET里简易实现IoC

.NET里简易实现IoC

前言

在前面的篇幅中对依赖倒置原则和IoC框架的使用只是做了个简单的介绍,并没有很详细的去演示,可能有的朋友还是区分不了依赖倒置、依赖注入、控制反转这几个名词,或许知道的也只是知道依赖倒置是原则,依赖注入、控制反转都是实现的方式,我将在下面对这些个名词做详细的介绍,在篇幅的最后还会自己实现了IoC容器的功能。

 

依赖倒置原则

我们先来看一段代码,代码1-1

    public class Top
    {
        public void Execution()
        {
            Underly underly = new Underly();
            underly.WriterLine();
        }
    }

    public class Underly
    {
        public void WriterLine()
        {
            Console.WriteLine("这是底层类型的输出");
        }
    }

从代码1-1中看到Top类型的Execution()方法中包含了对Underly的依赖,直接使用的New来实例化Underly类型,致使两个类型之间的耦合是属于强耦合类型,这样做会导致在需求发生变化的时候对于底层类型也就是Underly的修改会牵动到Top中的现实,而我们是不希望这种事情发生。

 

这个时候我们再看依赖原则的定义(度娘的):

A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。

B.抽象不应该依赖于具体,具体应该依赖于抽象。

 

A.高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象

对于A,照着字面意思来说的话很简单了,已经没法办再用文字来描述了,看代码吧,

代码1-2

    public class Top
    {
        public void Execution()
        {
            IUnderly underly = new Underly();
            underly.WriterLine();
        }
    }

    public interface IUnderly
    {
        void WriterLine();
    }

    public class Underly:IUnderly
    {
        public void WriterLine()
        {
            Console.WriteLine("这是底层类型的输出");
        }
    }

在代码1-2中我们对Underly进行了抽象,并且让其依赖于抽象(也就是实现接口),而在Top类型中也依赖于抽象了。

图1

图1中所示的就是代码1-2所要表示的类型结构了,也就是依赖倒置原则中A的实现,从图1中我们可以看到依赖倒置原则里还装着开放封闭原则,这里所要说明的意思就是依赖倒置原则是开放封闭原则的基础。

从图1中的结构来看,如果是站在开放封闭原则的角度来看也是没有问题的,对扩展开放对修改关闭,在需求变动的时候只要重新实现个依赖于抽象的下层,利用多态则可实现对扩展开放。

如果是站在依赖倒置原则的角度去看,那就是符合了依赖倒置原则定义的A条。

(Ps:这里有的朋友可能会说上面的示例中Top也依赖于具体了,我只想说请注意你的人身安全,我这个人脾气不太好。

开个玩笑,对于Top也依赖于具体的事确实是有的,后面会有说明)

 

B.抽象不应该依赖于具体,具体应该依赖于抽象

对于依赖倒置原则定义的B来说,我分两个部分来给大家解释一下。

第一个部分就是抽象不应该依赖于具体, 我们还是通过代码来说明吧。

代码1-3

    public interface IUnderly
    {
        void WriterLine();
        IWriter CreateWriterInstance();
    }

    public class Underly:IUnderly
    {
        public void WriterLine()
        {
            CreateWriterInstance().WriterLine();
        }
        public IWriter CreateWriterInstance()
        {
            return new Writer();
        }
    }

    public interface IWriter
    {
        void WriterLine();
    }

    public class Writer : IWriter
    {
        public void WriterLine()
        {
            Console.WriteLine("这只是一个输出");
        }
    }

首先我们新定义了一种输出方式Writer和它的抽象IWriter接口类型,我们想把它应用到Underly类型的输出中,然后我们修改了Underly的抽象类型也就是在IUnderly接口类型中新添加了一个CreateWriterInstance()方法,并且这个方法的返回类型是Writer的抽象,这样就对应了依赖倒置原则定义中B条的前半句话:抽象不应该依赖于具体。

错误的示范,代码1-4

    public interface IUnderly
    {
        void WriterLine();
        Writer CreateWriterInstance();
    }

这里这样的坏处很多后果也很严重,就不去细说了慢慢体会一下应该会感觉得到。

从依赖倒置原则定义中B条的前半句话中来看,我们可以在硕大的.NET Framework中看一下一些抽象的定义中是否有依赖于具体的,应该是没有至少我是没发现。

 

对于B条定义的后半句话,也就是第二个部分:具体应该依赖于抽象,这部分的内容就是约束我们在实际运用设计原则的时候会出现的问题,就好比上面的Top类型依然是依赖于具体了。

对于怎么解决这样的一个问题,有的朋友可能已经想到了,对的那就是依赖注入,都说依赖注入是依赖倒置原则的实现方式,这是不完全的,依赖注入解决的问题是将具体到具体的依赖转换成具体到抽象的依赖。我是这么认为的,纯属个人观点。

(ps:这是一种治标不治本的方法,DI把对象耦合的问题抛到了外部,也就是这样才导致了IoC的诞生,后面会有介绍。)

 

依赖注入

图2

对于上节中的示例中对象所依赖的图示。为了能像图1中所示的结构那样以及符合依赖倒置原则的定义,我们将使用依赖注入的方式,先暂时性的解决这样的问题。

依赖注入有三种方式,意思都差不多都是讲外部抽象的引用设置到内部来从而实现注入。

这里就简单示例一下,

构造函数注入

代码1-5

    public class Top
    {
        private IUnderly _Underly;
        public Top(IUnderly underly)
        {
            _Underly = underly;
        }

        public void Execution()
        {
            _Underly.WriterLine();
        }
    }
    public interface IUnderly
    {
        void WriterLine();
    }
    public class Underly:IUnderly
    {
        public void WriterLine()
        {
            Console.WriteLine("这只是一个底层类型的输出");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Top top = new Top(new Underly());
            top.Execution();

            Console.ReadLine();
        }
    }

如代码1-5所示那样,在Top类型的构造函数中定义了下层类型的抽象作为参数,以此达到依赖注入的目的。结果如图3。

图3

 

属性注入

代码1-6

    public class Top
    {
        private IUnderly _Underly;
        public Top() { }
        public Top(IUnderly underly)
        {
            _Underly = underly;
        }

        public IUnderly Underly
        {
            get { return _Underly; }
            set { _Underly = value; }
        }

        public void Execution()
        {
            _Underly.WriterLine();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Top top = new Top();
            top.Underly = new Underly();
            top.Execution();

            Console.ReadLine();
        }
    }

通过在内部设置属性来获取到底层抽象的引用,结果如图3.

 

接口注入

代码1-7

    public interface IQuote
    {
        void SetQuote(IUnderly underly);
    }

    public class Top:IQuote
    {
        private IUnderly _Underly;
        public Top() { }
        public Top(IUnderly underly)
        {
            _Underly = underly;
        }

        public IUnderly Underly
        {
            get { return _Underly; }
            set { _Underly = value; }
        }

        public void Execution()
        {
            _Underly.WriterLine();
        }

        public void SetQuote(IUnderly underly)
        {
            _Underly = underly;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Top top = new Top();
            top.SetQuote(new Underly());
            top.Execution();

            Console.ReadLine();
        }
    }

接口注入的方式原理还是一样的,让Top实现定义了设置引用方法的接口,依然是将外部的底层抽象引用设置到内部来,结果还是一样如图3.

 

这样虽说没什么问题了,但也只是局部的没有问题,我们看一下上面三个示例中Program类型中的测试代码,

图4

绕了一圈依赖注入是把耦合的问题抛到了外部,抛到了要使用Top类型的对象中,这个问题就很严重了,我们怎么解决呢?没关系通过IoC容器来实现。

 

自定义实现简易IoC

这一小节就来解决上述的问题,Ioc又叫控制反转,控制就是执行过程上的控制,反转是往哪转呢?

图5

从图5中我们可以看到,客户端调用IoC容器,并且在IoC容器中执行依赖注入操作,最后返回上层对象交给客户端,所以控制反转是由在客户端的控制权交由IoC容器,在IoC容器中进行依赖注入的操作后返回已达到控制权反转的目的,从来消弱对象间的耦合程度。

 那么IoC容器要做哪些工作呢?

图6

核心功能:生成依赖注入过程中的上层对象

基础流程:

1.需要向IoC容器中注册依赖注入过程中抽象、具体。

2.在使用IoC的时候需向IoC中注册上层对象的类型。

3.解析上层对象类型,并且执行生成对象操作

4.返回上层对象实例

 

功能对象定义:

1.抽象、具体关系维护的对象,用以维护依赖注入过程中抽象、具体的对应关系。

2.解析对象类型的对象,根据依赖注入的几种方式分析对象类型的构造和公共属性并且生成,(公共属性是符合IoC框架中定义的标准)。

3.公共属性标准对象,用以通知IoC框架上层对象中哪些公共属性需要被注入。

4.执行过程对象,用以表示框架执行流程,框架入口点。

初步就这样定了,有可能下面定义的类型中上面没有定义到,但是不妨碍,知道基础流程就行了。那现在就开始吧。

首先我们要定义IoC框架入口点,

代码1-8

namespace FrameWork.IoC.Achieve.IoCAbstractBasics
{
    public interface IIoCKernel
    {
        IIoCKernel Bind<T>();
        IIoCKernel To<U>() where U : class;
        V GetValue<V>() where V : class;
    }
}

对于IIoCKernel类型的定义,Bind和To两个方法用于绑定抽象、具体到关系维护的对象中,而GetValue()方法则是用以获取上层对象的实例,对于这种入口点的使用方式我是模仿的Ninject框架,会在最后的示例中演示怎么使用。(因为我只用过这一个,还是个半吊子只是简单的用过)

下面我们就来实现一下IIoCKernel,示例代码1-9.

代码1-9

using FrameWork.IoC.Achieve.IoCAbstractBasics;


namespace FrameWork.IoC.Achieve.IoCBasics
{
    public class IoCKernel : IIoCKernel
    {
        private Type _BaseType;

        public IoCKernel()
        {
            IoCContext.Context.DITyoeInfoManage = new DITypeInfoManage();
        }

        public IIoCKernel Bind<T>()
        {
            _BaseType = typeof(T);
            return this;
        }

        public IIoCKernel To<U>() where U : class
        {
            Type achieveType = typeof(U);
            if (achieveType.BaseType == _BaseType||achieveType.GetInterface(_BaseType.Name)!=null)
            {
                IoCContext.Context.DITyoeInfoManage.AddTypeInfo(_BaseType, achieveType);
            }
            return this;
        }

        public V GetValue<V>() where V : class
        {
           return IoCContext.Context.DITypeAnalyticalProvider.CreteDITypeAnalaytical().GetValue<V>();
        }
    }
}

在代码1-9中,IoCKernel实现了IIoCKernel接口,首先在其构造函数中,我们对抽象、具体关系维护的对象进行了初始化,并且设置到了当前IoC框架的上下文中,我们这里先停一下,看一下抽象、具体关系维护对象的构造,示例代码1-10.

代码1-10

namespace FrameWork.IoC.Achieve.IoCBasics
{
    /// <summary>
    /// DI类型关系信息管理
    /// </summary>
    public class DITypeInfoManage
    {
        private Dictionary<Type, Type> _DITypeInfo;
        public DITypeInfoManage()
        {
            _DITypeInfo = new Dictionary<Type, Type>();
        }

        /// <summary>
        /// 添加DI类型关系
        /// </summary>
        /// <param name="key">抽象类型</param>
        /// <param name="value">实现类型</param>
        public void AddTypeInfo(Type key, Type value)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }
            if (_DITypeInfo.ContainsKey(key))
            {
                return;
            }
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }
            _DITypeInfo.Add(key, value);
        }

        /// <summary>
        /// 获取DI类型关系的实现类型
        /// </summary>
        /// <param name="key">抽象类型</param>
        /// <returns></returns>
        public Type GetTypeInfo(Type key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }
            if (_DITypeInfo.ContainsKey(key))
            {
                return _DITypeInfo[key];
            }
            return null;
        }

        public bool ContainsKey(Type key)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }
            return _DITypeInfo.ContainsKey(key);
        }
    }
}

DITypeInfoManage类型对象表示着抽象、具体类型关系的信息维护,实则就是在内部封装了键值队,这里就不多说了,然后我们再看一下代码1-9中IoC框架入口点类型的构造函数中初始化DITypeInfoManage类型设置的上下文对象,来看示例代码1-11.

using FrameWork.IoC.Achieve.IoCAbstractBasics;
using FrameWork.IoC.Achieve.Providers;
using FrameWork.IoC.Achieve.IoCBasics;

namespace FrameWork.IoC.Achieve
{
    public class IoCContext
    {
        private IoCContext() { }

        private static IoCContext _Context;

        public static IoCContext Context
        {
            get 
            {
                if (_Context == null)
                {
                    _Context = new IoCContext();
                }
                return _Context;
            }
        }

        private IDITypeAnalyticalProvider _DITypeAnalyticalProvider;

        public IDITypeAnalyticalProvider DITypeAnalyticalProvider
        {
            get
            {
                if (_DITypeAnalyticalProvider == null)
                {
                    _DITypeAnalyticalProvider = new DefualtDITypeAnalyticalProivder();
                }
                return _DITypeAnalyticalProvider;
            }
            set
            {
                _DITypeAnalyticalProvider = value;
            }
        }

        private DITypeInfoManage _DITypeInfoManage;

        public DITypeInfoManage DITyoeInfoManage
        {
            get
            {
                return _DITypeInfoManage;
            }
            set
            {
                _DITypeInfoManage = value;
            }
        }
    }
}

代码1-11中的定义的IoCContext说是上下文对象,说是这么说,用以维护框架中必要的信息,实则就是一个单例模式的对象,但是意义上它还是上下文对象,在这个对象里面维护着所要依赖注入的抽象、具体类型维护的对象,这个对象我们上面代码1-10看过了,还有一个就是分析上层类型的提供程序对象,分析上层类型的提供程序对象是用以生成分析上层类型对象的,这样做便于对外扩展,我们就这样顺着往下看,看一下分析上层类型的提供程序对象,示例代码1-12。

代码1-12

using FrameWork.IoC.Achieve.IoCAbstractBasics;

namespace FrameWork.IoC.Achieve.Providers
{
    public interface IDITypeAnalyticalProvider
    {
        IDITypeAnalytical CreteDITypeAnalaytical();
    }
}

这里的IDITypeAnalytical接口类型就是分析类型的抽象,在提供程序抽象中用以它来做返回类型,这也遵循着依赖倒置原则B条的抽象不依赖于具体。现在我们来看一下默认实现,示例代码1-13.

代码1-13

using FrameWork.IoC.Achieve.IoCAbstractBasics;
using FrameWork.IoC.Achieve.IoCBasics;


namespace FrameWork.IoC.Achieve.Providers
{
    public class DefualtDITypeAnalyticalProivder:IDITypeAnalyticalProvider
    {
        public IDITypeAnalytical CreteDITypeAnalaytical()
        {
            return new DITypeAnalytical();
        }
    }
}

在代码1-13中定义的就是默认的分析上层类型提供程序了,默认返回的就是我们框架中默认的分析上层类型对象,现在我们就来看一下分析上层类型对象的抽象和具体实现,示例代码1-14。

代码1-14

namespace FrameWork.IoC.Achieve.IoCAbstractBasics
{
    public interface IDITypeAnalytical
    {
        T GetValue<T>();
    }
}

只是定义了一个泛型的GetValue()方法,泛型类型当然就是所需要执行依赖注入并且生成的上层对象类型了,这里没什么好说的,直接来看分析上层类型的具体实现吧,示例代码1-15.

代码1-15

using FrameWork.IoC.Achieve.IoCAbstractBasics;
using System.Reflection;

namespace FrameWork.IoC.Achieve.IoCBasics
{
    public class DITypeAnalytical : IDITypeAnalytical
    {

        public T GetValue<T>()
        {
            Type type = typeof(T);
            return (T)TypeAnalytical(type);
        }

        private object TypeAnalytical(Type type)
        {
            ConstructorInfo[] constructorInfos = type.GetConstructors();
            object instance = null;
            #region 构造函数注入
            foreach (ConstructorInfo conInfo in constructorInfos)
            {
                if (conInfo.GetParameters().Length > 0)
                {
                    ParameterInfo[] paras = conInfo.GetParameters();
                    List<object> args = new List<object>();

                    foreach (ParameterInfo para in paras)
                    {
                        if (IoCContext.Context.DITyoeInfoManage.ContainsKey(para.ParameterType))
                        {
                            object par = TypeAnalytical(IoCContext.Context.DITyoeInfoManage.GetTypeInfo(para.ParameterType));
                            args.Add(par);
                        }
                    }
                    instance = CreateInstance(type, args.ToArray());
                    break;
                }
            }
            #endregion
            if (instance == null)
            {
                instance = CreateInstance(type);
            }
            #region 属性注入
            if (type.GetProperties().Length > 0)
            {
                PropertyInfo[] proertyInfos = type.GetProperties();
                foreach (PropertyInfo propertyInfo in proertyInfos)
                {
                    if (propertyInfo.GetCustomAttributes(typeof(DITypeAttribute), false).Length > 0)
                    {
                        if (IoCContext.Context.DITyoeInfoManage.ContainsKey(propertyInfo.PropertyType))
                        {
                            object propertyvalue = TypeAnalytical(IoCContext.Context.DITyoeInfoManage.GetTypeInfo(propertyInfo.PropertyType));
                            propertyInfo.SetValue(instance, propertyvalue, null);
                        }
                    }
                }
            }
            #endregion

            return instance;
        }

        private object CreateInstance(Type type,params object[] args)
        {
            return Activator.CreateInstance(type, args);
        }
    }
}

在代码1-15的定义中,主要的核心功能在TypeAnalytical()方法中,这里主要说明一下这个方法的执行过程,首先是根据方法参数传入的类型,这个类型就是要实现依赖注入的类型,为什么不说这个参数类型是上层类型?

是因为在首先执行的过程中传入的是上层类型,然后判断其类型的构造函数,读取构造函数的参数类型根据【抽象、具体类型的维护对象】来查找当前上层类型是否需要进行构造函数依赖,如果【抽象、具体类型的维护对象】中存在所需的类型,则对上层类型的构造函数参数类型进行实例创建,并且再次调用TypeAnalytical()方法,因为我们不能确定上层类型构造函数的参数类型是否需要进行依赖注入,所以这里是递归的。

在创建完上层类型构造函数的参数类型实例后,便会对上层类型进行实例创建,因为这是依赖注入中构造函数注入的一种方式。

在此完毕后判断TypeAnalytical()方法中instance实例是否为空,如果是空的则说明上层类型没有采取构造函数注入的方式,在此我们还是要创建它的实例,以便下面的进行属性注入时对实例属性的赋值。

之后我们会对上层类型的所有公共属性根据条件进行查找,查找符合我们定义标准的公共属性,也就是DITypeAttribute类型,这个类型下面会贴出示例代码,假使在找到需要依赖注入的公共属性后执行过程便和上面执行构造函数注入的方式相同。

(ps:这里功能的定义并不是很严谨,而且只针对了构造函数注入和属性注入两种方式,并没有对接口注入提供支持。)

下面我们看一下上面所说的属性注入的特性类定义(也就是框架定义的规范),示例代码1-16.

代码1-16

namespace FrameWork.IoC.Achieve.IoCBasics
{
    [AttributeUsage(AttributeTargets.Property,AllowMultiple=false,Inherited=false)]
    public class DITypeAttribute:Attribute
    {
        public DITypeAttribute() { }
    }
}

就是一个简单的特性类定义,用作规范约束。

 

最后我们看一下测试用例:

代码1-17

using FrameWork.IoC.Achieve.IoCBasics;
using FrameWork.IoC.Achieve.IoCAbstractBasics;

using FrameWork.IoC.Case;
using FrameWork.IoC.Case.Test.TestOne;
using FrameWork.IoC.Case.Test.TestTwo;

namespace FrameWork.IoC.Case.Test
{
    public class DITest
    {
        private IAbstractOne _AbstractOne;
        public DITest(IAbstractOne abstractone)
        {
            _AbstractOne = abstractone;
        }

        private IAbstractTwo _AbstractTwo;

        [DIType]
        public IAbstractTwo AbstractTwo
        {
            get
            {
                return _AbstractTwo;
            }
            set
            {
                _AbstractTwo = value;
            }
        }

        public void Writer(string meg)
        {
            _AbstractOne.WriterLine(meg);
            _AbstractTwo.WriterLine(meg);
        }
    }
}

代码1-17定义中对DITest分别进行了构造函数、属性注入,注入类型分别对应着IAbstractOne、IAbstractTwo。我们先来看一下IAbstractOne抽象、具体的定义,示例代码1-18

代码1-18

namespace FrameWork.IoC.Case.Test.TestOne
{
    public interface IAbstractOne
    {
        void WriterLine(string meg);
    }

    public class AchieveOne:IAbstractOne
    {
        private IAbstractOne_One _AbstractOne_One;
        public AchieveOne(IAbstractOne_One abstractone)
        {
            _AbstractOne_One = abstractone;
        }


        private IAbstractOne_Two _AbstractOne_Two;

        [DIType]
        public IAbstractOne_Two AbstractOne_Two
        {
            get
            {
                return _AbstractOne_Two;
            }
            set
            {
                _AbstractOne_Two = value;
            }
        }

        public void WriterLine(string meg)
        {
            _AbstractOne_One.WirterLine(meg);
            _AbstractOne_Two.WriterLine(meg);
            Console.WriteLine(meg + "-This is TestOne");
        }
    }

}

代码1-18中定义了IAbstractOne抽象、AchieveOne具体实现,并且在AchieveOne具体实现中还对IAbstractOne_One、IAbstractOne_Two分别进行了构造函数、属性注入。从最上层来看就是嵌套的注入,这样更能体现出IoC框架的重要性。

我们看一下IAbstractOne_One、IAbstractOne_Two类型的抽象、具体定义,示例代码1-19.

代码1-19

namespace FrameWork.IoC.Case.Test.TestOne
{
    public interface IAbstractOne_One
    {
        void WirterLine(string meg);
    }

    public class AbstractOne_One:IAbstractOne_One
    {
        public void WirterLine(string meg)
        {
            Console.WriteLine(meg + "-This is TestOne_One");
        }
    }

    public interface IAbstractOne_Two
    {
        void WriterLine(string meg);
    }

    public class AbstractOne_Two:IAbstractOne_Two
    {
        public void WriterLine(string meg)
        {
            Console.WriteLine(meg + "-This is TestOne_Two");
        }
    }

}

最后我们再看一下IAbstractTwo抽象和具体实现的定义,示例代码1-20.

代码1-20

namespace FrameWork.IoC.Case.Test.TestTwo
{
    public interface IAbstractTwo
    {
        void WriterLine(string meg);
    }

    public class AchieveTwo:IAbstractTwo
    {
        public void WriterLine(string meg)
        {
            Console.WriteLine(meg + "-This is TestTwo");
        }
    }

}

真的是最后我们看一下客户端的调用代码,示例代码1-21,

代码1-21

using FrameWork.IoC.Achieve.IoCBasics;
using FrameWork.IoC.Achieve.IoCAbstractBasics;

using FrameWork.IoC.Case;
using FrameWork.IoC.Case.Test;
using FrameWork.IoC.Case.Test.TestOne;
using FrameWork.IoC.Case.Test.TestTwo;

namespace FrameWork.IoC.Case
{
    class Program
    {
        static void Main(string[] args)
        {
            #region IoCTest
            IIoCKernel iocKernel = new IoCKernel();
            iocKernel.Bind<IAbstractOne>().To<AchieveOne>();
            iocKernel.Bind<IAbstractTwo>().To<AchieveTwo>();
            iocKernel.Bind<IAbstractOne_One>().To<AbstractOne_One>();
            iocKernel.Bind<IAbstractOne_Two>().To<AbstractOne_Two>();
            DITest diType = iocKernel.GetValue<DITest>();
            diType.Writer("IoCFrameWorkTest");
            #endregion
            Console.ReadLine();
        }
    }
}

最后看一下结果,如图7

图7

到这里本篇的内容就结束了,自定义IoC只是一个参考,并没有对IoC框架进行深入的实现,只是以此做一个引导,建议大家还是选择一款合适的IoC框架当作学习的对象,当然感兴趣的朋友还是可以自己写的。

搬砖不易,且搬且用心,感谢各位工友的支持,谢谢大家。

 

 

作者:金源

出处:http://www.cnblogs.com/jin-yuan/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面

posted @ 2014-07-04 08:30  金源  阅读(12089)  评论(39编辑  收藏  举报