`
varsoft
  • 浏览: 2503983 次
  • 性别: Icon_minigender_1
  • 来自: 上海
文章分类
社区版块
存档分类
最新评论

[原创] Enterprise Library深入解析与灵活应用(6):自己动手创建迷你版AOP框架

阅读更多

基于Enterprise Library PIAB的AOP框架已经在公司项目开发中得到广泛的使用,但是最近同事维护一个老的项目,使用到了Enterprise Library 2,所以PIAB是在Enterprise Library 3.0中推出的,所以不同直接使用。为了解决这个问题,我写了一个通过方法劫持(Method Interception)的原理,写了一个简易版的AOP框架。(如果对PIAB不是很了解的读者,可以参阅我的文章MS Enterprise Library Policy Injection Application Block 深入解析[总结篇])。 Souce Code下载:http://files.cnblogs.com/artech/Artech.SimpleAopFramework.zip

一、如何使用?

编程方式和PIAB基本上是一样的,根据具体的需求创建相应的CallHandler,通过Custom Attribute的形式将CallHandler应用到类型或者方法上面。下面就是一个简单例子。

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->usingSystem;
usingSystem.Collections.Generic;
usingSystem.Data.Common;
usingSystem.Data.SqlClient;
usingArtech.SimpleAopFramework.Demos;
usingSystem.Configuration;
namespaceArtech.SimpleAopFramework
{
classProgram
{
staticvoidMain(string[]args)
{
stringuserID=Guid.NewGuid().ToString();
InstanceBuilder.Create
<UserManager,IUserManager>().CreateDuplicateUsers(userID,Guid.NewGuid().ToString());
Console.WriteLine(
"IstheuserwhoseIDis\"{0}\"hasbeensuccessfullycreated!{1}",userID,UserUtility.UserExists(userID)?"Yes":"No");
}
}

publicclassUserManager:IUserManager
{
[ExceptionCallHandler(Ordinal
=1,MessageTemplate="Encountererror:\nMessage:{Message}")]
[TransactionScopeCallHandler(Ordinal
=2)]
publicvoidCreateDuplicateUsers(stringuserID,stringuserName)
{
UserUtility.CreateUser(userID,userName);
UserUtility.CreateUser(userID,userName);
}
}

publicinterfaceIUserManager
{
voidCreateDuplicateUsers(stringuserID,stringuserName);
}
}
在上面例子中,我创建了两个CallHandler:TransactionScopeCallHandler和ExceptionCallHandler,用于进行事务和异常的处理。也就是说,我们不需要手工地进行事务的Open、Commit和Rollback的操作,也不需要通过try/catch block进行手工的异常处理。为了验证正确性,我模拟了这样的场景:数据库中有一个用户表(Users)用于存储用户帐户,每个帐户具有唯一ID,现在我通过UserManager的CreateDuplicateUsers方法插入两个具有相同ID的记录,毫无疑问,如果没有事务的处理,第一次用户添加将会成功,第二次将会失败。反之如果我们添加的TransactionScopeCallHandler能够起作用,两次操作将在同一个事务中进行,重复的记录添加将会导致怎过事务的回退。

ExceptionCallHandler中,会对抛出的SqlException进行处理,在这我们仅仅是打印出异常相关的信息。至于具有要输出那些信息,可以通过ExceptionCallHandlerAttribute的MessageTemplate 属性定义一个输出的模板。

运行程序,我们会得到这样的结果,充分证明了事务的存在,错误信息也按照我们希望的模板进行输出。

image

二、设计概要

同PIAB的实现原理一样,我通过自定义RealProxy实现对CallHandler的执性,从而达到方法调用劫持的目的(底层具体的实现,可以参阅我的文章Policy Injection Application Block 设计和实现原理)。下面的UML列出了整个框架设计的所有类型。

image

  • ICallHandler:所有CallHandler必须实现的接口。
  • CallHandlerBase:实现了ICallHandler的一个抽象类,是自定义CallHandler的基类。
  • HandlerAttribute:所有的CallHandler通过相应的HandlerAttribute被应用到所需的目标对象上。HandlerAttribute是一个继承自Attribute的抽象类,是自定义HandlerAttribute的基类。
  • CallHandlerPipeline:由于同一个目标方法上面可以同时应用多个CallHandler,在运行时,他们被串成一个有序的管道,依次执行。
  • InterceptingRealProxy<T>:继承自RealProxy,CallHandlerPipeline最终在Invoke方法中执行,从而实现了“方法调用劫持”。
  • InvocationContext:表示当前方法执行的上下文,Request和Reply成员表示方法的调用和返回消息。
  • InstanceBuidler:由于我们需根据InterceptingRealProxy<T>对象创建TransparentProxy,并通过TransparentProxy进行方法的调用,CallHandler才能在RealProxy中被执行。InstanceBuilder用于方便的创建TransparentProxy对象。

三、具体实现

现在我们来详细分析实现的细节。下来看看表示方法调用上下文的InvocationContext的定义。

1、InvocationContext

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->usingSystem.Collections.Generic;
usingSystem.Runtime.Remoting.Messaging;

namespaceArtech.SimpleAopFramework
{
///<summary>
///InvocationContextrepresentsthecontextofamethodinvocation.
///</summary>
publicclassInvocationContext
{
///<summary>
///A<seecref="IMethodCallMessage"/>objectrepresentsthemethodcall.
///</summary>
publicIMethodCallMessageRequest
{
get;set;}

///<summary>
///A<seecref="ReturnMessage"/>objectrepresentsthereturnofthemethodcall.
///</summary>
publicReturnMessageReply
{
get;set;}

///<summary>
///A<seecref="IDictionary<object,object>"/>objectusedtosetextracontextualinformation.
///</summary>
publicIDictionary<object,object>Properties
{
get;set;}
}
}

Request和Reply本质上都是一个System.Runtime.Remoting.Messaging.IMessage对象。Request是IMethodCallMessage 对象,表示方法调用的消息,Reply则是ReturnMessage对象,具有可以包含具体的返回值,也可以包含抛出的异常。Properties可以供我们自由地设置一些自定义的上下文。

2、ICallHandler、CallHandlerBase和HandlerAttribute

ICallHandler包含四个成员,PreInvoke和PostInvoke在执行目标方法前后被先后调用,自定义CallHandler可以根据自己的具体需求实现这个两个方法。PreInvoke返回值可以通过PostInvoke的correlationState获得。Ordinal表明CallHandler在CallHandler管道的位置,他决定了应用于同一个目标方法上的多个CallHandler的执行顺序。ReturnIfError表示CallHandler在抛出异常时是否直接退出。

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->namespaceArtech.SimpleAopFramework
{
///<summary>
///Allofthecustomcallhandlermustdireclyorindirectlyimpplementthisinterface.
///</summary>
publicinterfaceICallHandler
{
///<summary>
///Thismethodwillbeinjectedintotheinvocationstackpriortoinvoketargetmethod.
///</summary>
///<paramname="context">A<seecref="InvocationContext"/>objectindicatingthecurrentmethodinvocationcontext.</param>
///<returns>TheuserstatewhichcanbepickedupinthePostInvokemethod.</returns>
objectPreInvoke(InvocationContextcontext);

///<summary>
///Thismethodwillbeinjectedintotheinvocationstackaftertargetmethodisinvoked.
///</summary>
///<paramname="context">A<seecref="InvocationContext"/>objectindicatingthecurrentmethodinvocationcontext.</param>
///<paramname="correlationState">TheuserstatereturnedfromthePreinvokemethod.</param>
voidPostInvoke(InvocationContextcontext,objectcorrelationState);

///<summary>
///A<seecref="int"/>valueindicatingthepositionwherethecallhandlerisplacedinthepipeline.
///</summary>
intOrdinal
{
get;set;}

///<summary>
///A<seecref="bool"/>valueindicatingwhethertodirectlyreturnifencounteringerror.
///</summary>
boolReturnIfError
{
get;set;}
}
}

CallHandler的抽象基类CallHandlerBase仅仅是对ICallHandler的简单实现。

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->namespaceArtech.SimpleAopFramework
{
///<summary>
///Thebaseclassofallcustomcallhandler.
///</summary>
publicabstractclassCallHandlerBase:ICallHandler
{
#regionICallHandlerMembers

///<summary>
///Themethodwillbeinjectedintotheinvocationstackpriortoexecutethetargetmethod.
///</summary>
///<paramname="context">A<seecref="InvocationContext"/>objectpresentingthecurrentmethodinvocationcontext.</param>
///<returns>AstateobjectwhichcanbepickupbyPostInvokemethod.</returns>
publicabstractobjectPreInvoke(InvocationContextcontext);

///<summary>e
///Themethodwillbeinjectedintotheinvicationstackafterthetargetmethodisinvoked.
///</summary>
///<paramname="context">A<seecref="InvocationContext"/>objectpresentingthecurrentmethodinvocationcontext.</param>
///<paramname="correlationState">AsateobjectreturnedfromPreInvokemethod.</param>
publicabstractvoidPostInvoke(InvocationContextcontext,objectcorrelationState);

///<summary>
///A<seecref="int"/>valueindicatingthepositionwherethecallhandlerisplacedinthepipeline.
///</summary>
publicintOrdinal
{
get;set;}

///<summary>
///A<seecref="bool"/>valueindicatingwhethertodirectlyreturnifencounteringerror.
///</summary>
publicboolReturnIfError
{
get;set;}
#endregion
}
}

HandlerAttribute中定义了CreateCallHandler方法创建相应的CallHandler对象,Ordinal和ReturnIfError同上。

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->usingSystem;
namespaceArtech.SimpleAopFramework
{
///<summary>
///Thebaseclassofcustomcallhandlerattribute.
///</summary>
publicabstractclassHandlerAttribute:Attribute
{
///<summary>
///Allofthesubclassmustimplementthismethodtocreate<seecref="ICallHandler"/>object.
///</summary>
///<returns>Thecreated<seecref="ICallHandler"/>object</returns>
publicabstractICallHandlerCreateCallHandler();

///<summary>
///A<seecref="int"/>valueindicatingthepositionwherethecallhandlerisplacedinthepipeline.
///</summary>
publicintOrdinal
{
get;set;}

///<summary>
///A<seecref="bool"/>valueindicatingwhethertodirectlyreturnifencounteringanerror.
///</summary>
publicboolReturnIfError
{
get;set;}
}
}

3、CallHandlerPipeline

CallHandlerPipeline是CallHandler的有序集合,我们通过一个IList<ICallHandler> 对象和代码最终目标对象的创建CallHandlerPipeline。CallHandlerPipeline的核心方法是Invoke。在Invoke方法中按照CallHandler在管道中的次序先执行PreInvoke方法,然后通过反射执行目标对象的相应方法,最后逐个执行CallHandler的PostInvoke方法。

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Runtime.Remoting.Messaging;

namespaceArtech.SimpleAopFramework
{
publicclassCallHandlerPipeline
{
privateobject_target;
privateIList<ICallHandler>_callHandlers;

publicCallHandlerPipeline(objecttarget)
:
this(newList<ICallHandler>(),target)
{}

publicCallHandlerPipeline(IList<ICallHandler>callHandlers,objecttarget)
{
if(target==null)
{
thrownewArgumentNullException("target");
}

if(callHandlers==null)
{
thrownewArgumentNullException("callHandlers");
}

this._target=target;
this._callHandlers=callHandlers;
}

///<summary>
///Invokethecallhandlerandthetargetinstaneonebyone.
///</summary>
///<paramname="context"></param>
publicvoidInvoke(InvocationContextcontext)
{
Stack
<object>correlationStates=newStack<object>();
Stack
<ICallHandler>callHandlerStack=newStack<ICallHandler>();

//Preinvoke.
foreach(ICallHandlercallHandlerinthis._callHandlers)
{
correlationStates.Push(callHandler.PreInvoke(context));
if(context.Reply!=null&&context.Reply.Exception!=null&&callHandler.ReturnIfError)
{
context.Reply
=newReturnMessage(context.Reply.Exception,context.Request);
return;
}
callHandlerStack.Push(callHandler);
}

//InvokeTargetObject.
object[]copiedArgs=Array.CreateInstance(typeof(object),context.Request.Args.Length)asobject[];
context.Request.Args.CopyTo(copiedArgs,
0);
try
{
objectreturnValue=context.Request.MethodBase.Invoke(this._target,copiedArgs);
context.Reply
=newReturnMessage(returnValue,copiedArgs,copiedArgs.Length,context.Request.LogicalCallContext,context.Request);
}
catch(Exceptionex)
{
context.Reply
=newReturnMessage(ex,context.Request);
}

//PostInvoke.
while(callHandlerStack.Count>0)
{
ICallHandlercallHandler
=callHandlerStack.Pop();
objectcorrelationState=correlationStates.Pop();
callHandler.PostInvoke(context,correlationState);
}
}

///<summary>
///SortallofthecallhandlerbasedonOrderinal.
///</summary>
publicvoidSort()
{
ICallHandler[]callHandlers
=this._callHandlers.ToArray<ICallHandler>();
ICallHandlerswaper
=null;
for(inti=0;i<callHandlers.Length-1;i++)
{
for(intj=i+1;j<callHandlers.Length;j++)
{
if(callHandlers[i].Ordinal>callHandlers[j].Ordinal)
{
swaper
=callHandlers[i];
callHandlers[i]
=callHandlers[j];
callHandlers[j]
=swaper;
}
}
}

this._callHandlers=callHandlers.ToList<ICallHandler>();
}

///<summary>
///Comineanew<seecref="CallHandlerPipeline"/>object.
///</summary>
///<paramname="pipeline">The<seecref="CallHandlerPipeline"/>objecttocombine.</param>
publicvoidCombine(CallHandlerPipelinepipeline)
{
if(pipeline==null)
{
thrownewArgumentNullException("pipeline");
}

foreach(ICallHandlercallHandlerinpipeline._callHandlers)
{
this.Add(callHandler);
}
}

///<summary>
///Combineanew<seecref="IList<ICallHandler>"/>object.
///</summary>
///<paramname="callHandlers">The<seecref="IList<ICallHandler>"/>objecttocombine.</param>
publicvoidCombine(IList<ICallHandler>callHandlers)
{
if(callHandlers==null)
{
thrownewArgumentNullException("callHandlers");
}

foreach(ICallHandlercallHandlerincallHandlers)
{
this.Add(callHandler);
}
}

///<summary>
///Addanew<seecref="ICallHandler"/>objecttothecallhandlerpipeline.
///</summary>
///<paramname="callHandler">The<seecref="ICallHandler"/>objecttoadd.</param>
///<returns>The<seecref="ICallHandler"/>objecttoadd.</returns>
publicICallHandlerAdd(ICallHandlercallHandler)
{
if(callHandler==null)
{
thrownewArgumentNullException("callHandler");
}

this._callHandlers.Add(callHandler);
returncallHandler;
}
}
}

4、InterceptionRealProxy<T>

InterceptingRealProxy<T>是现在AOP的关键所在,我们通过一个IDictionary<MemberInfo, CallHandlerPipeline>和目标对象创建InterceptingRealProxy对象。在Invoke方法中,根据方法表示方法调用的IMethodCallMessage对象的MethodBase为key,从CallHandlerPipeline字典中获得基于当前方法的CallHandlerPipeline,并调用它的Invoke方法,InvocationContext的Reply即为最终的返回。

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->usingSystem;
usingSystem.Collections.Generic;
usingSystem.Runtime.Remoting.Proxies;
usingSystem.Runtime.Remoting.Messaging;
usingSystem.Reflection;
namespaceArtech.SimpleAopFramework
{
///<summary>
///TheInterceptingRealProxyisacustom<seecref="RealProxy"/>usedtoperformmethodinterception.
///</summary>
///<typeparamname="T">Thetyperepresentingthedeclarationtypeofthetransparentproxy.</typeparam>
publicclassInterceptingRealProxy<T>:RealProxy
{
privateIDictionary<MemberInfo,CallHandlerPipeline>_callHandlerPipelines;

publicInterceptingRealProxy(objecttarget,IDictionary<MemberInfo,CallHandlerPipeline>callHandlerPipelines)
:
base(typeof(T))
{
if(callHandlerPipelines==null)
{
thrownewArgumentNullException("callHandlerPipelines");
}

this._callHandlerPipelines=callHandlerPipelines;
}

///<summary>
///Themethodinvocaitonisinterceptedandalloftherelatedcallhandlersisinvoked.
///</summary>
///<paramname="msg">A<seecref="IMessage"/>representingmethodcall.</param>
///<returns>A<seecref="IMessage"/>representigthereturnofmethodinvocation.</returns>
publicoverrideIMessageInvoke(IMessagemsg)
{
InvocationContextcontext
=newInvocationContext();
context.Request
=(IMethodCallMessage)msg;
this._callHandlerPipelines[context.Request.MethodBase].Invoke(context);
returncontext.Reply;
}
}
}

5、InstanceBuidler

同PIAB通过PolicyInjection.Create()/Wrap()创建Transparent Proxy类型,InstanceBuidler也充当这样的工厂功能。InstanceBuidler的实现原理就是:通过反射获得目标类型上所有的HandlerAttribute,通过调用HandlerAttribute的CreateCallHandler创建相应的CallHandler。对于每个具体的方法,将应用在其类和方法上的所有的CallHandler组合成CallHandlerPipeline,然后以MemberInfo对象为Key将所有基于某个方法的CallHandlerPipeline构成一个CallHandlerPipeline字典。该字典,连同通过反射创建的目标对象,创建InterceptingRealProxy<T>对象。最后返回InterceptingRealProxy<T>对象的TransparentProxy对象。

Code
<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />-->usingSystem;
usingSystem.Collections.Generic;
usingSystem.Reflection;
namespaceArtech.SimpleAopFramework
{
///<summary>
///InstanceBuidlerisusedtocreatetheobjectwhichcanbeintercepted.
///</summary>
publicclassInstanceBuilder
{
///<summary>
///Createanobjectwhichcanbeintercepted.
///</summary>
///<typeparamname="TObject">Thetypeofthetargetobj
分享到:
评论

相关推荐

    Enterprise Library 4.1 安装程序

    Enterprise Library 是微软 Patterns & Practices 团队开发的一个开源软件开发框架,主要针对企业级应用程序的构建,它提供了多个可重用的、针对常见设计模式的库。在4.1版本中,这个框架继续为.NET开发者提供了强大...

    Enterprise Library 5.0 源码

    8. 源码结构与设计模式:Enterprise Library 5.0 的源码还展示了良好的代码组织和设计模式的应用,如工厂模式、策略模式、装饰器模式等,这对于理解和实践面向对象设计原则非常有帮助。 通过研究 Enterprise ...

    Microsoft Enterprise Library 5.0

    - **可扩展性**:Enterprise Library 的每个应用块都设计成可以扩展的,允许开发者根据需求定制和扩展功能。 - **灵活性**:通过配置文件,开发者可以轻松调整应用块的行为,而无需修改代码。 - **最佳实践**:遵循...

    C# 实现 AOP微型框架

    6. **C#实现**:在VS2008中,实现AOP框架可能涉及到使用`System.Reflection`来获取类型信息,然后利用`System.Reflection.Emit`或第三方库创建动态代理类,实现对方法调用的拦截和通知的插入。 7. **微型框架**:与...

    Microsoft EnterPrise Library 5.0安装包

    6. **集成开发环境支持**:Enterprise Library通常提供了对Visual Studio等IDE的集成,使得在项目中添加和配置Enterprise Library的组件变得直观且简单。 在使用Enterprise Library 5.0时,重要的是理解每个库的...

    仿 Spring 手写 IoC、AOP 框架.rar

    在IT领域,Spring框架是Java开发中的一个基石,尤其在企业级应用开发中扮演着重要角色。Spring的主要功能包括依赖注入(IoC)和面向切面编程(AOP)。本教程将带你深入理解这两个概念,并通过手写一个简易的IoC和AOP...

    Enterprise Library 资料

    6. **配置工具**:Enterprise Library 配置工具允许开发者通过直观的界面来配置各个应用块,而无需编写代码,使得配置和维护更加简便。 7. **政策注入应用块**:Policy Injection 应用块支持面向切面编程(AOP),...

    C# 实现的AOP框架

    在创建自己的C# AOP框架时,首先需要理解以下几个关键概念: 1. **切面(Aspect)**:切面是关注点的模块化,比如日志记录、事务管理等,它们通常跨多个对象和类。 2. **连接点(Join Point)**:程序中可以插入切...

    AspectJ in Action: Enterprise AOP with Spring Applications

    ### AspectJ in Action: Enterprise AOP with Spring Applications #### 关键知识点概述 1. **Spring-AspectJ集成:**本书重点介绍了Spring框架与AspectJ相结合的技术优势及其在企业级应用中的强大功能。 2. **...

    EnterpriseLibrary系列课程1概述第3集

    1. **Application Blocks**:EnterpriseLibrary的核心组成部分是Application Blocks,它们是一组可重用的代码组件,有助于简化和标准化常见的开发任务。例如,Data Access Application Block提供了一种方便的方式来...

    Enterprise Library 5.0

    Enterprise Library 5.0是微软开发的一个开源软件框架,它主要针对企业级应用程序的开发,提供了许多可重用的、经过验证的软件组件,旨在简化常见的编程任务,并提高开发效率。这个版本在2010年发布,是该库系列的一...

    Enterprise Library 3.1的 中文文档

    Enterprise Library 3.1是微软 Patterns & Practices 团队开发的一个开源软件开发库,它为.NET Framework 提供了一组可重用的、面向企业级应用的软件构建块。这个中文文档是针对该版本的详细指南,帮助开发者理解和...

    仿springAOP框架

    在仿Spring AOP框架中,你可以创建自己的注解,如`@LogBefore`、`@Transactional`等,这些注解用于标识需要在执行前进行日志记录或需要事务管理的方法。注解的实现通常涉及处理器(Aspect),它们会在程序运行时扫描...

    Enterprise Library 4.1 学习资料

    Enterprise Library 4.1 是微软 Patterns & Practices 团队开发的一个强大的软件开发框架,它为.NET Framework 提供了一套可重用的、企业级的应用程序基础组件。这个学习资料包可能包含了关于如何使用和理解 ...

    简易的AOP框架

    在Java中,Spring框架是最著名的AOP实现之一,但这里我们讨论的是一个简易的AOP框架,它可以帮助理解AOP的基本概念和工作原理。 该简易AOP框架包含以下几个关键组件: 1. **配置文件**:这是定义切面(aspect)和...

    SSM框架:深入解析与应用.pdf

    ### SSM框架:深入解析与应用 #### 一、SSM框架概述 SSM框架,全称为Spring + SpringMVC + MyBatis的组合,是当前用于开发Java Web应用程序的一种非常流行的框架组合。该框架结合了Spring框架的核心功能、...

    C#实现的IOC和AOP框架,供学习

    学习这个框架,开发者可以深入理解IOC和AOP的概念,掌握如何在C#中实现和使用这两种模式,提升自己的编程技巧和软件设计能力。同时,了解和使用开源框架,也有助于扩展视野,了解业界最佳实践。

    Microsoft.Practices.EnterpriseLibrary5.0几个dll

    在.NET开发领域,Microsoft.Practices.EnterpriseLibrary(简称Enterprise Library)是微软推出的一套企业级应用程序开发框架,它提供了多种可重用的软件组件,旨在简化常见的编程任务,如数据访问、日志记录、异常...

    Spring技术内幕:深入解析Spring架构与设计原理(第2版) .pdf

    《Spring技术内幕:深入解析Spring架构与设计原理(第2版)》这本书主要聚焦于Spring框架的核心架构和技术细节,帮助读者全面理解Spring的工作机制、设计理念以及实现方式。下面将根据书名及其描述来展开相关知识点。 ...

Global site tag (gtag.js) - Google Analytics