• C# Autofac学习笔记


        一、为什么使用Autofac?

        Autofac是.NET领域最为流行的IoC框架之一,传说是速度最快的一个。

        1.1、性能

        有人专门做了测试:

        1.2、优点

        1)与C#语言联系很紧密。C#里的很多编程方式都可以为Autofac使用,例如可以使用Lambda表达式注册组件。

        2)较低的学习曲线。学习它非常的简单,只要你理解了IoC和DI的概念以及在何时需要使用它们。

        3)支持JSON/XML配置。

        4)自动装配。

        5)与Asp.Net MVC集成。

        6)微软的Orchad开源程序使用的就是Autofac,可以看出它的方便和强大。

        1.3、资源

        官方网站:http://autofac.org/

        GitHub网址:https://github.com/autofac/Autofac

        学习资料:Autofac中文文档

        二、数据准备

        2.1、新建项目

        IService下的接口类:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace LinkTo.Test.Autofac.IService
    {
        /// <summary>
        /// 动物吠声接口类
        /// </summary>
        public interface IAnimalBark
        {
            /// <summary>
            /// 吠叫
            /// </summary>
            void Bark();
        }
    }
    IAnimalBark
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace LinkTo.Test.Autofac.IService
    {
        /// <summary>
        /// 动物睡眠接口类
        /// </summary>
        public interface IAnimalSleep
        {
            /// <summary>
            /// 睡眠
            /// </summary>
            void Sleep();
        }
    }
    IAnimalSleep
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace LinkTo.Test.Autofac.IService
    {
        /// <summary>
        /// 学校接口类
        /// </summary>
        public interface ISchool
        {
            /// <summary>
            /// 放学
            /// </summary>
            void LeaveSchool();
        }
    }
    ISchool
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace LinkTo.Test.Autofac.IService
    {
        /// <summary>
        /// 学生接口类
        /// </summary>
        public interface IStudent
        {
            /// <summary>
            /// 增加学生
            /// </summary>
            /// <param name="studentID">学生ID</param>
            /// <param name="studentName">学生姓名</param>
            void Add(string studentID, string studentName);
        }
    }
    IStudent

        Service下的接口实现类:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using LinkTo.Test.Autofac.IService;
    
    namespace LinkTo.Test.Autofac.Service
    {
        /// <summary>
        /// 猫类
        /// </summary>
        public class Cat : IAnimalSleep
        {
            /// <summary>
            /// 睡眠
            /// </summary>
            public void Sleep()
            {
                Console.WriteLine("小猫咪睡着了zZ");
            }
        }
    }
    Cat
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using LinkTo.Test.Autofac.IService;
    
    namespace LinkTo.Test.Autofac.Service
    {
        /// <summary>
        /// 狗类
        /// </summary>
        public class Dog : IAnimalBark, IAnimalSleep
        {
            /// <summary>
            /// 吠叫
            /// </summary>
            public void Bark()
            {
                Console.WriteLine("汪汪汪");
            }
    
            /// <summary>
            /// 睡眠
            /// </summary>
            public void Sleep()
            {
                Console.WriteLine("小狗狗睡着了zZ");
            }
        }
    }
    Dog
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using LinkTo.Test.Autofac.IService;
    
    namespace LinkTo.Test.Autofac.Service
    {
        /// <summary>
        /// 学校类
        /// </summary>
        public class School : ISchool
        {
            /// <summary>
            /// IAnimalBark属性
            /// </summary>
            public IAnimalBark AnimalBark { get; set; }
    
            /// <summary>
            /// 放学
            /// </summary>
            public void LeaveSchool()
            {
                AnimalBark.Bark();
                Console.WriteLine("你家的熊孩子放学了⊙o⊙");
            }
        }
    }
    School
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using LinkTo.Test.Autofac.IService;
    
    namespace LinkTo.Test.Autofac.Service
    {
        /// <summary>
        /// 学生类
        /// </summary>
        public class Student : IStudent
        {
            /// <summary>
            /// 无参构造函数
            /// </summary>
            public Student()
            { }
    
            /// <summary>
            /// 有参构造函数
            /// </summary>
            /// <param name="studentID">学生ID</param>
            /// <param name="studentName">学生姓名</param>
            public Student(string studentID, string studentName)
            {
                Add(studentID, studentName);
            }
    
            /// <summary>
            /// 增加学生
            /// </summary>
            /// <param name="studentID">学生ID</param>
            /// <param name="studentName">学生姓名</param>
            public void Add(string studentID, string studentName)
            {
                Console.WriteLine($"新增的学生是:{studentName}");
            }
        }
    }
    Student
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using LinkTo.Test.Autofac.IService;
    
    namespace LinkTo.Test.Autofac.Service
    {
        /// <summary>
        /// 动物摇尾巴
        /// </summary>
        public class AnimalWagging
        {
            /// <summary>
            /// IAnimalBark属性
            /// </summary>
            IAnimalBark animalBark;
    
            /// <summary>
            /// 有参构造函数
            /// </summary>
            /// <param name="bark">IAnimalBark变量</param>
            public AnimalWagging(IAnimalBark bark)
            {
                animalBark = bark;
            }
    
            /// <summary>
            /// 摇尾巴
            /// </summary>
            public virtual void Wagging()
            {
                animalBark.Bark();
                Console.WriteLine("摇尾巴");
            }
    
            /// <summary>
            /// 计数
            /// </summary>
            /// <returns></returns>
            public static int Count()
            {
                return 6;
            }
    
            /// <summary>
            /// 任务
            /// </summary>
            /// <param name="name">动物名称</param>
            /// <returns></returns>
            public virtual async Task<string> WaggingAsync(string name)
            {
                var result = await Task.Run(() => Count());
                return $"{name}摇了{result}下尾巴";
            }
        }
    }
    AnimalWagging

        2.2、Autofac安装

        Client项目右键->管理 NuGet 程序包->Autofac。

        三、IoC-注册

        3.1、类型注册

        a)类型注册:使用RegisterType进行注册。

                //注册Autofac组件
                ContainerBuilder builder = new ContainerBuilder();
                //注册实现类Student,当我们请求IStudent接口的时候,返回的是类Student的对象。
                builder.RegisterType<Student>().As<IStudent>();
                //上面这句也可改成下面这句,这样请求Student实现了的任何接口的时候,都会返回Student对象。
                //builder.RegisterType<Student>().AsImplementedInterfaces();
                IContainer container = builder.Build();
                //请求IStudent接口
                IStudent student = container.Resolve<IStudent>();
                student.Add("1001", "Hello");
    View Code

        b)类型注册(别名):假如一个接口有多个实现类,可以在注册时起别名。

                ContainerBuilder builder = new ContainerBuilder();
                builder.RegisterType<Dog>().Named<IAnimalSleep>("Dog");
                builder.RegisterType<Cat>().Named<IAnimalSleep>("Cat");
                IContainer container = builder.Build();
    
                var dog = container.ResolveNamed<IAnimalSleep>("Dog");
                dog.Sleep();
                var cat = container.ResolveNamed<IAnimalSleep>("Cat");
                cat.Sleep();
    View Code

        c)类型注册(枚举):假如一个接口有多个实现类,也可以使用枚举的方式注册。

            public enum AnimalType
            {
                Dog,
                Cat
            }
    View Code
                ContainerBuilder builder = new ContainerBuilder();
                builder.RegisterType<Dog>().Keyed<IAnimalSleep>(AnimalType.Dog);
                builder.RegisterType<Cat>().Keyed<IAnimalSleep>(AnimalType.Cat);
                IContainer container = builder.Build();
    
                var dog = container.ResolveKeyed<IAnimalSleep>(AnimalType.Dog);
                dog.Sleep();
                var cat = container.ResolveKeyed<IAnimalSleep>(AnimalType.Cat);
                cat.Sleep();
    View Code

        3.2、实例注册

                ContainerBuilder builder = new ContainerBuilder();
                builder.RegisterInstance<IStudent>(new Student());
                IContainer container = builder.Build();
    
                IStudent student = container.Resolve<IStudent>();
                student.Add("1001", "Hello");
    View Code

        3.3、Lambda注册

        a)Lambda注册

                ContainerBuilder builder = new ContainerBuilder();
                builder.Register(c => new Student()).As<IStudent>();
                IContainer container = builder.Build();
    
                IStudent student = container.Resolve<IStudent>();
                student.Add("1001", "Hello");
    View Code

        b)Lambda注册(NamedParameter)

                ContainerBuilder builder = new ContainerBuilder();
                builder.Register<IAnimalSleep>((c, p) =>
                    {
                        var type = p.Named<string>("type");
                        if (type == "Dog")
                        {
                            return new Dog();
                        }
                        else
                        {
                            return new Cat();
                        }
                    }).As<IAnimalSleep>();
                IContainer container = builder.Build();
    
                var dog = container.Resolve<IAnimalSleep>(new NamedParameter("type", "Dog"));
                dog.Sleep();
    View Code

        3.4、程序集注册

        如果有很多接口及实现类,假如觉得这种一一注册很麻烦的话,可以一次性全部注册,当然也可以加筛选条件。

                ContainerBuilder builder = new ContainerBuilder();
                Assembly assembly = Assembly.Load("LinkTo.Test.Autofac.Service");   //实现类所在的程序集名称
                builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces();  //常用
                //builder.RegisterAssemblyTypes(assembly).Where(t=>t.Name.StartsWith("S")).AsImplementedInterfaces();  //带筛选
                //builder.RegisterAssemblyTypes(assembly).Except<School>().AsImplementedInterfaces();  //带筛选
                IContainer container = builder.Build();
    
                //单实现类的用法
                IStudent student = container.Resolve<IStudent>();
                student.Add("1001", "Hello");
    
                //多实现类的用法
                IEnumerable<IAnimalSleep> animals = container.Resolve<IEnumerable<IAnimalSleep>>();
                foreach (var item in animals)
                {
                    item.Sleep();
                }
    View Code

        3.5、泛型注册

                ContainerBuilder builder = new ContainerBuilder();
                builder.RegisterGeneric(typeof(List<>)).As(typeof(IList<>));
                IContainer container = builder.Build();
    
                IList<string> list = container.Resolve<IList<string>>();
    View Code

        3.6、默认注册

                ContainerBuilder builder = new ContainerBuilder();
                //对于同一个接口,后面注册的实现会覆盖之前的实现。
                //如果不想覆盖的话,可以用PreserveExistingDefaults,这样会保留原来注册的实现。
                builder.RegisterType<Dog>().As<IAnimalSleep>();
                builder.RegisterType<Cat>().As<IAnimalSleep>().PreserveExistingDefaults();  //指定为非默认值
                IContainer container = builder.Build();
    
                var dog = container.Resolve<IAnimalSleep>();
                dog.Sleep();
    View Code

        四、IoC-注入

        4.1、构造函数注入

                ContainerBuilder builder = new ContainerBuilder();
                builder.RegisterType<AnimalWagging>();
                builder.RegisterType<Dog>().As<IAnimalBark>();
                IContainer container = builder.Build();
    
                AnimalWagging animal = container.Resolve<AnimalWagging>();
                animal.Wagging();
    View Code

        4.2、属性注入

                ContainerBuilder builder = new ContainerBuilder();
                Assembly assembly = Assembly.Load("LinkTo.Test.Autofac.Service");                           //实现类所在的程序集名称
                builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces().PropertiesAutowired();    //常用
                IContainer container = builder.Build();
    
                ISchool school = container.Resolve<ISchool>();
                school.LeaveSchool();
    View Code

        五、IoC-事件

        Autofac在组件生命周期的不同阶段,共对应了5个事件,执行顺序如下所示:

        1.OnRegistered->2.OnPreparing->3.OnActivating->4.OnActivated->5.OnRelease

                ContainerBuilder builder = new ContainerBuilder();
                builder.RegisterType<Student>().As<IStudent>()
                    .OnRegistered(e => Console.WriteLine("OnRegistered:在注册的时候调用"))
                    .OnPreparing(e => Console.WriteLine("OnPreparing:在准备创建的时候调用"))
                    .OnActivating(e => Console.WriteLine("OnActivating:在创建之前调用"))
                    //.OnActivating(e => e.ReplaceInstance(new Student("1000", "Test")))
                    .OnActivated(e => Console.WriteLine("OnActivated:在创建之后调用"))
                    .OnRelease(e => Console.WriteLine("OnRelease:在释放占用的资源之前调用"));
                using (IContainer container = builder.Build())
                {
                    IStudent student = container.Resolve<IStudent>();
                    student.Add("1001", "Hello");
                }
    View Code

        六、IoC-生命周期

        6.1、Per Dependency

        Per Dependency:为默认的生命周期,也被称为"transient"或"factory",其实就是每次请求都创建一个新的对象。

                ContainerBuilder builder = new ContainerBuilder();
                Assembly assembly = Assembly.Load("LinkTo.Test.Autofac.Service");                                                   //实现类所在的程序集名称
                builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces().PropertiesAutowired().InstancePerDependency();    //常用
                IContainer container = builder.Build();
    
                ISchool school1 = container.Resolve<ISchool>();
                ISchool school2 = container.Resolve<ISchool>();
                Console.WriteLine(school1.Equals(school2));
    View Code

        6.2、Single Instance

        Single Instance:就是每次都用同一个对象。

                ContainerBuilder builder = new ContainerBuilder();
                Assembly assembly = Assembly.Load("LinkTo.Test.Autofac.Service");                                           //实现类所在的程序集名称
                builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces().PropertiesAutowired().SingleInstance();   //常用
                IContainer container = builder.Build();
    
                ISchool school1 = container.Resolve<ISchool>();
                ISchool school2 = container.Resolve<ISchool>();
                Console.WriteLine(ReferenceEquals(school1, school2));
    View Code

        6.3、Per Lifetime Scope

        Per Lifetime Scope:同一个Lifetime生成的对象是同一个实例。

                ContainerBuilder builder = new ContainerBuilder();
                builder.RegisterType<School>().As<ISchool>().InstancePerLifetimeScope();
                IContainer container = builder.Build();
                ISchool school1 = container.Resolve<ISchool>();
                ISchool school2 = container.Resolve<ISchool>();
                Console.WriteLine(school1.Equals(school2));
                using (ILifetimeScope lifetime = container.BeginLifetimeScope())
                {
                    ISchool school3 = lifetime.Resolve<ISchool>();
                    ISchool school4 = lifetime.Resolve<ISchool>();
                    Console.WriteLine(school3.Equals(school4));
                    Console.WriteLine(school2.Equals(school3));
                }
    View Code

        七、IoC-通过配置文件使用Autofac

        7.1、组件安装

        Client项目右键->管理 NuGet 程序包->Autofac.Configuration及Microsoft.Extensions.Configuration.Xml。

        7.2、配置文件

        新建一个AutofacConfigIoC.xml文件,在其属性的复制到输出目录项下选择始终复制。

    <?xml version="1.0" encoding="utf-8" ?>
    <autofac defaultAssembly="LinkTo.Test.Autofac.IService">
      <!--无注入-->
      <components name="1001">
        <type>LinkTo.Test.Autofac.Service.Student, LinkTo.Test.Autofac.Service</type>
        <services name="0" type="LinkTo.Test.Autofac.IService.IStudent" />
        <injectProperties>true</injectProperties>
      </components>
      <components name="1002">
        <type>LinkTo.Test.Autofac.Service.Dog, LinkTo.Test.Autofac.Service</type>
        <services name="0" type="LinkTo.Test.Autofac.IService.IAnimalBark" />
        <injectProperties>true</injectProperties>
      </components>
      <!--构造函数注入-->
      <components name="2001">
        <type>LinkTo.Test.Autofac.Service.AnimalWagging, LinkTo.Test.Autofac.Service</type>
        <services name="0" type="LinkTo.Test.Autofac.Service.AnimalWagging, LinkTo.Test.Autofac.Service" />
        <injectProperties>true</injectProperties>
      </components>
      <!--属性注入-->
      <components name="3001">
        <type>LinkTo.Test.Autofac.Service.School, LinkTo.Test.Autofac.Service</type>
        <services name="0" type="LinkTo.Test.Autofac.IService.ISchool" />
        <injectProperties>true</injectProperties>
      </components>
    </autofac>
    View Code

        7.3、测试代码

                //加载配置
                ContainerBuilder builder = new ContainerBuilder();
                var config = new ConfigurationBuilder();
                config.AddXmlFile("AutofacConfigIoC.xml");
                var module = new ConfigurationModule(config.Build());
                builder.RegisterModule(module);
                IContainer container = builder.Build();
                //无注入测试
                IStudent student = container.Resolve<IStudent>();
                student.Add("1002", "World");
                //构造函数注入测试
                AnimalWagging animal = container.Resolve<AnimalWagging>();
                animal.Wagging();
                //属性注入测试
                ISchool school = container.Resolve<ISchool>();
                school.LeaveSchool();
    View Code

        八、AOP 

        8.1、组件安装

        Client项目右键->管理 NuGet 程序包->Autofac.Extras.DynamicProxy。

        8.2、拉截器

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Threading.Tasks;
    using Castle.DynamicProxy;
    
    namespace LinkTo.Test.Autofac.Client
    {
        /// <summary>
        /// 拦截器:需实现IInterceptor接口。
        /// </summary>
        public class CallLogger : IInterceptor
        {
            private readonly TextWriter _output;
    
            public CallLogger(TextWriter output)
            {
                _output = output;
            }
    
            /// <summary>
            /// 拦截方法:打印被拦截的方法--执行前的名称、参数以及执行后的返回结果。
            /// </summary>
            /// <param name="invocation">被拦截方法的信息</param>
            public void Intercept(IInvocation invocation)
            {
                //空白行
                _output.WriteLine();
    
                //在下一个拦截器或目标方法处理之前的处理
                _output.WriteLine($"调用方法:{invocation.Method.Name}");
    
                if (invocation.Arguments.Length > 0)
                {
                    _output.WriteLine($"参数:{string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())}");
                }
    
                //调用下一个拦截器(若存在),直到最终的目标方法(Target Method)。
                invocation.Proceed();
    
                //获取被代理方法的返回类型
                var returnType = invocation.Method.ReturnType;
    
                //异步方法
                if (IsAsyncMethod(invocation.Method))
                {
                    //Task:返回值是固定类型
                    if (returnType != null && returnType == typeof(Task))
                    {
                        //定义一个异步方法来等待目标方法返回的Task
                        async Task Continuation() => await (Task)invocation.ReturnValue;
                        //Continuation()中并没有使用await,所以Continuation()就如同步方法一样是阻塞的。
                        invocation.ReturnValue = Continuation();
                    }
                    //Task<T>:返回值是泛型类型
                    else
                    {
                        //获取被代理方法的返回类型
                        var returnTypeT = invocation.Method.ReflectedType;
                        if (returnTypeT != null)
                        {
                            //获取泛型参数集合,集合中的第一个元素等价于typeof(Class)。
                            var resultType = invocation.Method.ReturnType.GetGenericArguments()[0];
                            //利用反射获得等待返回值的异步方法
                            MethodInfo methodInfo = typeof(CallLogger).GetMethod("HandleAsync", BindingFlags.Public | BindingFlags.Instance);
                            //调用methodInfo类的MakeGenericMethod()方法,用获得的类型T(<resultType>)来重新构造HandleAsync()方法。
                            var mi = methodInfo.MakeGenericMethod(resultType);
                            //Invoke:使用指定参数调用由当前实例表示的方法或构造函数。
                            invocation.ReturnValue = mi.Invoke(this, new[] { invocation.ReturnValue });
                        }
                    }
    
                    var type = invocation.Method.ReturnType;
                    var resultProperty = type.GetProperty("Result");
    
                    if (resultProperty != null)
                        _output.WriteLine($"方法结果:{resultProperty.GetValue(invocation.ReturnValue)}");
                }
                //同步方法
                else
                {
                    if (returnType != null && returnType != typeof(void))
                        _output.WriteLine($"方法结果:{invocation.ReturnValue}");
                }
            }
    
            /// <summary>
            /// 判断是否异步方法
            /// </summary>
            public static bool IsAsyncMethod(MethodInfo method)
            {
                return 
                    (
                        method.ReturnType == typeof(Task) || 
                        (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
                    );
            }
    
            /// <summary>
            /// 构造等待返回值的异步方法
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="task"></param>
            /// <returns></returns>
            public async Task<T> HandleAsync<T>(Task<T> task)
            {
                var t = await task;
                return t;
            }
        }
    }
    CallLogger
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Castle.DynamicProxy;
    
    namespace LinkTo.Test.Autofac.Client
    {
        public class CallTester: IInterceptor
        {
            public void Intercept(IInvocation invocation)
            {
                Console.WriteLine("啥也不干");
                invocation.Proceed();
                Console.WriteLine("也不干啥");
            }
        }
    }
    CallTester

        8.3、测试代码

        注意:对于以类方式的注入,Autofac Interceptor要求类的方法必须为virtual方法。如AnimalWagging类的Wagging()、WaggingAsync(string name)都加了virtual修饰符。

                ContainerBuilder builder = new ContainerBuilder();
    
                //注册拦截器
                builder.Register(c => new CallLogger(Console.Out));
                builder.Register(c => new CallTester());
    
                //动态注入拦截器
    
                //这里定义了两个拦截器,注意它们的顺序。
                builder.RegisterType<Student>().As<IStudent>().InterceptedBy(typeof(CallLogger), typeof(CallTester)).EnableInterfaceInterceptors();
    
                //这里定义了一个拦截器
                builder.RegisterType<AnimalWagging>().InterceptedBy(typeof(CallLogger)).EnableClassInterceptors();
                builder.RegisterType<Dog>().As<IAnimalBark>();
    
                IContainer container = builder.Build();
                IStudent student = container.Resolve<IStudent>();
                student.Add("1003", "Kobe");
    
                AnimalWagging animal = container.Resolve<AnimalWagging>();
                animal.Wagging();
    
                Task<string> task = animal.WaggingAsync("哈士奇");
                Console.WriteLine($"{task.Result}");
    View Code

        IoC参考自:

        https://www.xin3721.com/ArticlecSharp/c14013.html

        https://www.cnblogs.com/GoogleGetZ/p/10218721.html

        http://niuyi.github.io/blog/2012/04/06/autofac-by-unit-test/

        https://www.cnblogs.com/kissdodog/p/3611799.html

        AOP参考自:

        https://www.cnblogs.com/stulzq/p/6880394.html

        https://blog.csdn.net/weixin_38211198/article/details/105925821

        https://blog.csdn.net/q932104843/article/details/97611912

  • 相关阅读:
    js中location.href的用法
    entityframework单例模式泛型用法
    [C#]从URL中获取路径的最简单方法-new Uri(url).AbsolutePath
    等到花儿也谢了的await
    ASP.NET MVC下的异步Action的定义和执行原理
    实际案例:在现有代码中通过async/await实现并行
    ASP.NET MVC Controllers and Actions
    扩展HtmlHelper
    iOS开发网络篇—XML数据的解析
    IOS学习:常用第三方库(GDataXMLNode:xml解析库)
  • 原文地址:https://www.cnblogs.com/atomy/p/12834804.html
Copyright © 2020-2023  润新知