c#/.Net中实现AOP的两种方式(Spring.Net,PostSharp)

AOP就是面向切面编程,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

主要功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。

主要意图
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

AOP 术语

通知(Advice): 通知描述了切面要完成的任务,同时还描述了何时执行这个任务。

连接点(Joinpoint):  程序中应用通知的地方称为连接点,这个点可以是方法被调用时,异常抛出时,甚至访问属性的时候。

切入点(Pointcut): 切入点定义通知切入的一个或者多个连接点。

切面(Aspect): 切面就是通知和切入点的结合。通知和切入点共同定义了切面的全部内容:功能、时机、位置。

引入(Introduction): 允许我们想现有类中增加属性和方法.

目标(Target): 被通知的对象

代理(Porxy): 向目标对象增加通知之后,创建的对象,由这个对象来访问实际的目标对象。

织入(Weaving): 被切面应用到目标对象来创建新的代理对象的过程。

在JAVA中AOP也是Spring的一个重要内容。由于项目是C#的,项目中需要到AOP去进行权限拦截。所以就去了解了一下在C#/NET中实现AOP的办法,整理一下记录。

在C#/.NET中实现AOP简单点实现可以使用Spring.Net的框架中AOP以及PostSharp插件实现AOP。以VS2017为例。

一、Spring.Net的框架在C#/NET中实现AOP(动态织入)

Spring.Net的织入方式是动态织入,运行时AOP的实现方式是将扩展添加到运行虚拟机而不是编译器。Aspect和业务代码分别独立编译,而在运行时由虚拟机在必要时进行织入。大概示意图如下(简单画的):

 

先在项目中的添加Spring.Aop和Spring.Core两个引用,如图所示

然后就可以开始进行编写代码。

在spring有下面四种通知:

一、拦截环绕通知(around advice):Spring.NET中最基本的通知类型是拦截环绕通知(interception around advice),即方法拦截器。拦截环绕通知继承IMethodInterceptor接口。注意其中IMethodInvocation.Proceed()方法的调用。该方法会依次调用拦截器链上的其它拦截器。大部分拦截器都需要调用这个方法并返回它的返回值。当然,也可以不调用Proceed方法,而返回一个其它值或抛出一个异常,但一般不太会这么做。
二、前置通知(before advise):是在IMethodInterceptor.Proceed()方法调用前的通知。继承自IMethodBeforeAdvice接口。
三、异常通知(throws advise):是在IMethodInterceptor.Proceed()方法调用时发生异常的通知。继承自IthrowsAdvice接口。IthrowsAdvice接口没有定义任何方法:它是一个标识接口(按:之所以用标识接口,原因有二:1、在通知方法中,只有最后一个参数是必须的。如果声明为接口的方法,参数列表就被固定了。2、如果第一个原因可以用重载的接口方法解决,那么这个原因就是使用标识接口的充分原因了:实现此接口的类必须声明一或多个通知方法,接口方法做不到这一点),用以表明实现它的类声明了一或多个强类型的异常通知方法。
四、后置通知(after returning advise):是在IMethodInterceptor.Proceed()方法调用后的通知。继承自IAfterReturningAdvice接口。后置通知对切入点的执行没有影响,如果通知抛出异常,就会沿拦截器链向上抛出,从而中断拦截器链的继续执行。

注意:在Spring.Net框架中目标类必须是接口的实现类。在JAVA中的Spring中目标类可以是接口的实现,也可以是单独的类。我也一直在找官方的文档说明,可是官方网站(http://www.springframework.net/)当前的文档说明还是停留在1.3.2.。而我看从vs2017中添加引用的版本已经是2.0了。我看1.3.2的版本的文档中说这么一句话:

The current implementation of the AOP proxy generator uses object composition to delegate calls from the proxy to a target object, similar to how you would implement a classic Decorator pattern. This means that classes that need to be proxied have to implement one or more interfaces, which is in our opinion not only a less-intruding requirement than ContextBoundObject inheritance requirements, but also a good practice that should be followed anyway for the service classes that are most common targets for AOP proxies.

意思就是代理类(也就是目标类)必须去实现一个或多个接口。新版的我也没找到相关的文档说明。

首先我们应该去创建一个目标类PersonImpl,以及接口类person

namespace AopTest
{
    public interface person
    {
        void personSay(string name);
    }
}
namespace AopTest
{
    public class personImpl:person
    {
        public void personSay(string name)
        {
           Console.Out.WriteLine("personSay:"+name);
        }
    }
}

然后就应该去编写切面(通知)类 MyInterceptor去继承环绕通知,其他通知类似,可以自行编写实验:

namespace AopTest
{
    public class MyInterceptor : IMethodInterceptor
    {
        public object Invoke(IMethodInvocation invocation)
        {
            //可以看成前置通知
            Console.Out.WriteLine("spring_aop_before");
            object result = invocation.Proceed();
            //可以看成后置通知
            Console.Out.WriteLine("spring_aop_after");
            return result;
        }
    }
}

所有的类都准备好之后,接下来就是生成代理类了,在官方文档中有两种生成代理类的方式,一种是使用ProxyFactoryObject创建AOP代理,另一种就是ProxyFactory以编程方式创建AOP代理(个人不推荐,当然也有用的地方).

先说第一种使用ProxyFactoryObject创建AOP代理:

先去编写配置文件App.config,如果你的项目中没有此文件的话,就去在新建项中新建一个此文件:并且根据需要进行配置:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
  </configSections>
  <spring>

    <context>
      <resource uri="config://spring/objects" />
    </context>

    <objects xmlns='http://www.springframework.net'
         xmlns:db="http://www.springframework.net/database"
         xmlns:tx="http://www.springframework.net/tx"
         default-autowire="byName" default-lazy-init="true">
      <!--定义通知-->
      <object id="aroundAdvice" type="AopTest.MyInterceptor" />
      <!--创建代理类-->
      <object id="person" type="Spring.Aop.Framework.ProxyFactoryObject">
        <!--目标类-->
        <property name="Target">
          <object id = "isayTarget" type="AopTest.personImpl" />
        </property>
        <!--要应用的通知-->
        <property name="InterceptorNames">
          <list>
            <value>aroundAdvice</value>
          </list>
        </property>
      </object>
    </objects>
  </spring>
</configuration>

大家可以参考官方的文档说明进行配置。这个xml配置的含义就是让spring去为我们自动生成代理类。然后我们再进行一个测试方法:

static void Main(string[] args)
        {
            
            IApplicationContext ctx = ContextRegistry.GetContext();
            person person = (person)ctx["person"];
            person.personSay("www.b0c0.com");
        }

运行结果:

大家可以看到已经生效了。

第二种就是ProxyFactory以编程方式创建AOP代理:

这种方法就可以不需要配置文件,直接用ProxyFactory类来创建AOP代理,将测试代码更改如下:

static void Main(string[] args)
        {
            ProxyFactory proxyFactory = new ProxyFactory(new personImpl());
            proxyFactory.AddAdvice(new MyInterceptor());
            person personImpl = (person)proxyFactory.GetProxy();
            personImpl.personSay("www.b0c0.com");
           
        }

这样也能成功的实现。但是这样就无法使用与IOC结合使用,受限比较大。

这就是spring.Net框架来实现AOP,很方便,但是就是有一个我认为太局限的问题,就是目标类(代理类)必须要继承一个或多个接口。

二、使用PostSharp插件在C#/NET中来实现AOP(静态织入)

另一种就是用PostSharp插件来实现AOP,静态织入AOP的实现思想是给语言的编译器做扩展,使得在编译程序的时候编译器将相应的Aspect代码织入到业务代码的指定连接点,输出整合的结果。就是编译完成后的代码中,进行反编译查看的时候,会多出来一部分代码,而这多出来的代码就是PostSharp静态织入进去的。当然,这些代码在每次编译完成后,PostSharp都会重新织入一次。大概示意图如下(简单画的):

所以PostSharp的AOP没有了代理类必须继承接口的限制,这一点我感觉相对于Spring.Net中的AOP有很大的优势。

使用PostSharp插件的话一定要去下载安装包安装,只引用是不行的!。

这是https://www.postsharp.net/官方网站,可以在官网上下载安装包,最新的时6.0了。也可以直接再微软的Visual Studio 工具中下载。任意方式下载之后安装,然后重启vs将自动以插件形式扩展到vs中。

然后直接在项目上右键就会出现Add PostSharp to project,也就是添加PostSharp到你的项目中。然后就能使用了。

在PostSharp的AOP中,主要环绕通知、异常通知、方法边界通知、属性字段通知。

在PostSharp的通知类中必须加上 [Serializable]声明。

以及加上

[assembly: AopTest.MyMI(AttributeTargetTypes = “null”,
AttributeTargetTypeAttributes = MulticastAttributes.Public,
AttributeTargetMemberAttributes = MulticastAttributes.Public)]

具体含义就是:

AttributeTargetTypes=”要拦截过滤的类”;

AttributeTargetTypeAttributes 拦截的类属性

AttributeTargetMemberAttributes 拦截的方法属性

AttributeTargetMembers = “Dispose”,AttributeExclude = true 排除掉任何名为Dispose的方法。

同时我们用的时候除了上面的进行过滤还可以进行指定类或者方法进行特定的通知。只需在相应的类或者方法前加上[通知name]即可,具体请参照我写的实例。并且通知可组合使用。

还有一些可以去官方文档上查。

1、环绕通知:

在PostSharp环绕通知中,我们可以继承PostSharp中的MethodInterceptionAspect类来实现,这个类里面有一个主要的函数可以重载以实现包围整个方法体的作用,它是OnInvoke(MethodInterceptionArgs args),在它的内部可以通过base.OnInvoke(args)来调用我们加特性声明的方法执行流程,通过这个方法我们可以在方法开始调用前做操作,调用之后做操作,也就是可以当成前置通知和后置通知来使用。

先编写目标类person:

public class PostSharpTest
    {
        [MyMI]
        public string MITest(string name)
        {
            Console.Out.WriteLine("环绕通知测试");
            return name;
        }
    }

然后就是环绕通知类MyMIAttribute:

// This file contains registration of aspects that are applied to several classes of this project.
/*AttributeTargetTypes 要拦截过滤的类
 * AttributeTargetMembers = “Dispose”,AttributeExclude = true 排除掉任何名为Dispose的方法
 * 
*AttributeTargetTypeAttributes 拦截的类属性
*AttributeTargetMemberAttributes 拦截的方法属性
*/
[assembly: AopTest.MyMI(AttributeTargetTypes = "null",
    AttributeTargetTypeAttributes = MulticastAttributes.Public,
    AttributeTargetMemberAttributes = MulticastAttributes.Public)]
namespace AopTest
{
    /*针对整个方法体进行包围调用添加日志和截取异常(类似于spring中的环绕通知)
        * 
        * PostSharp中的MethodInterceptionAspect类是针对整个方法体的截取,
        * 继承于它的特性可以对整个方法体进行控制和日志截取、异步操作等!
        * 这个类里面有一个主要的函数可以重载以实现包围整个方法体截取的作用,
        * 它是OnInvoke(MethodInterceptionArgs args)。意义如下:
        * 在它的内部可以通过base.OnInvoke(args)来调用我们加特性声明的方法执行流程,
        * 通过这个方法我们可以在方法开始调用前做操作,调用之后做操作。
        */
    [Serializable]
    public class MyMIAttribute : MethodInterceptionAspect
    {
        public override void OnInvoke(MethodInterceptionArgs args)
        {
            Arguments arguments = args.Arguments;
            StringBuilder sb = new StringBuilder();
            ParameterInfo[] parameters = args.Method.GetParameters();
            for (int i = 0; arguments != null && i < arguments.Count; i++)
            {
                //进入的参数的值        
                sb.Append(parameters[i].Name + "=" + arguments[i] + "");
            }
            try
            {
                Console.WriteLine("进入{0}方法,参数是:{1}", args.Method.DeclaringType +"."+ args.Method.Name, sb.ToString());
                base.OnInvoke(args);
                Console.WriteLine("退出{0}方法,返回结果是:{1}", args.Method.DeclaringType + "." + args.Method.Name, args.ReturnValue);
            }
            catch (Exception ex)
            {
                Console.WriteLine(string.Format("出现异常,此方法异常信息是:{0}", ex.ToString()));
            }
        }
    }
}

测试类:

  static void Main(string[] args)
        {
           PostSharpTest ps = new PostSharpTest();
           ps.MITest("www.b0c0.com");
            
        }

运行结果:

 

2、异常通知:

在PostSharp异常通知中,我们可以继承PostSharp中的OnExceptionAspect类来实现,可以在发生异常的时候对方法体内的异常截取,并且做出动作,看是否停止本程序运行,还是忽略异常。特别注意:出现了异常之后的异常方法体的异常语句之后语句都不会执行,所以返回值也不会执行,所以我们可以在异常通知代码中指定返回值。

目标类PostSharpTest:

 public class PostSharpTest
    {
        public string ExceptionTest(string name)
        {
            Console.Out.WriteLine("异常通知测试" + name);
            throw new Exception("测试异常通知");
            //这一句一定不会执行
            Console.Out.WriteLine("已经成功获取数据");
            return "已经成功获取数据";
        }
    }

异常通知类MyExAttribute:

// This file contains registration of aspects that are applied to several classes of this project.
/*AttributeTargetTypes 要拦截过滤的类
 * AttributeTargetMembers = “Dispose”,AttributeExclude = true 排除掉任何名为Dispose的方法
 * 
*AttributeTargetTypeAttributes 拦截的类属性
*AttributeTargetMemberAttributes 拦截的方法属性
*/
[assembly: AopTest.MyEx(AttributeTargetTypes = "AopTest.*",
    AttributeTargetTypeAttributes = MulticastAttributes.Public,
    AttributeTargetMemberAttributes = MulticastAttributes.Public)]
namespace AopTest
{
    /*
     * OnExceptionAspect类是针对异常的消息截取。
     * 继承于它的特性将可以在发生异常的时候对方法体内的异常截取,并且做出动作,看是否停止本程序运行,还是忽略异常。
     * 这个类里面有两个主要的函数可以重载分别是
     * OnException(MethodExecutionArgs args)、GetExceptionType(System.Reflection.MethodBase targetMethod)。
     * OnException(MethodExecutionArgs args):当发生异常时截取异常发生的位置(在哪个命名空间?哪个类?哪个方法?)、异常类型、异常消息等信息,并且可以异常将如何处理。
     * GetExceptionType(System.Reflection.MethodBase targetMethod):设置需要拦截的异常类型,比如设置需要拦截参数异常,那么其他的异常类型发生时将不会被此特性所拦截。
     */
    [Serializable]
    public class MyExAttribute : OnExceptionAspect
    {
        //当异常发生时
        public override void OnException(MethodExecutionArgs args)
        {
            Console.WriteLine("______________________________________________________________________________");
            Console.WriteLine("异常时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); ;
            Console.WriteLine("异常类名:" + args.Method.DeclaringType.FullName);
            Console.WriteLine("异常方法:" + args.Method.Name);
            Console.WriteLine("异常信息:" + args.Exception.ToString());
            //出现了异常之后的异常方法体的异常语句之后语句都不会执行,所以返回值也不会执行
            args.ReturnValue = "异常通知定义的返回值";
            args.FlowBehavior = FlowBehavior.Continue;
        }

        //需要拦截的异常类型为ArgumentException
        public override Type GetExceptionType(System.Reflection.MethodBase targetMethod)
        {
            return typeof(Exception);
        }       
    }
}

测试方法:

static void Main(string[] args)
        {

            PostSharpTest ps = new PostSharpTest();
            ps.ExceptionTest("www.b0c0.com");
        }

运行结果:

3、方法边界通知

在PostSharp方法边界通知中,我们可以继承PostSharp中的OnMethodBoundaryAspect类来实现。继承于此类的特性A将围绕整个方法B进行静态注入式的处理,这个特性A可以重载覆写OnMethodBoundaryAspect类的OnEntry()、OnSuccess()、OnException()、OnExit()方法。可以控制进入方法时、方法执行成功时、发生异常时、退出方法时的处理。注意但是这个通知不能阻止方法体的执行。并且这个里面的OnException()根据官方文档说明与 OnExceptionAspect通知中的OnException(MethodExecutionArgs args)等效。

目标类PostSharpTest:

  public class PostSharpTest
    {
        [MyMB]
        public string MBTest(string name)
        {
            Console.Out.WriteLine("方法边界通知测试");
            throw new Exception("方法边界通知异常测试");
            return name;
        }
    }

方法边界通知类MyExAttribute:

// This file contains registration of aspects that are applied to several classes of this project.
/*AttributeTargetTypes 要拦截过滤的类
 * AttributeTargetMembers = “Dispose”,AttributeExclude = true 排除掉任何名为Dispose的方法
 * 
*AttributeTargetTypeAttributes 拦截的类属性
*AttributeTargetMemberAttributes 拦截的方法属性
*/
[assembly: AopTest.MyMB(AttributeTargetTypes = "null",
    AttributeTargetTypeAttributes = MulticastAttributes.Public,
    AttributeTargetMemberAttributes = MulticastAttributes.Public)]
namespace AopTest
{
    /* 针对方法边界的AOP处理类:
     * OnMethodBoundaryAspect
     * 继承于此类的特性A将围绕整个方法B进行静态注入式的处理,
     * 这个特性A可以重载覆写OnMethodBoundaryAspect类的OnEntry()、OnSuccess()、OnException()、OnExit()方法。
     */
    [Serializable]
    public class MyMBAttribute : OnMethodBoundaryAspect
    {
        //进入方法时
        public override void OnEntry(MethodExecutionArgs args)
        {
            Console.WriteLine("方法边界通知触发: 即将进入 [ {0} ] ...", args.Method);
            //执行进入方法
            base.OnEntry(args);
        }
        //方法执行成功时
        public override void OnSuccess(MethodExecutionArgs args)
        {
            Console.WriteLine("方法边界通知触发: 即将成功执行 [ {0} ] ...", args.Method);
            //执行方法执行成功
            base.OnSuccess(args);
        }
        //发生异常时
        public override void OnException(MethodExecutionArgs args)
        {
            Console.WriteLine("方法边界通知触发: 发生异常 [ {0} ] ...", args.Method);
            //跳过异常方法体执行
            args.FlowBehavior = FlowBehavior.Continue;
            base.OnException(args);
        }
        //退出方法时
        public override void OnExit(MethodExecutionArgs args)
        {
            Console.WriteLine("方法边界通知触发: 即将退出方法 [ {0} ] ...", args.Method);
            //退出方法时体执行
            base.OnExit(args);
        }
    }
}

测试方法:

static void Main(string[] args)
        {
            PostSharpTest ps = new PostSharpTest();
            ps.MBTest("www.b0c0.com");
        }

运行结果:

 

4、属性、字段通知

在PostSharp方法属性,字段通知中,我们可以继承PostSharp中的OnMethodBoundaryAspect类来实现。这个是非常强大的,我们可以截取初始化属性、设置属性、获取属性等时候的数据,来针对属性进行附加控制。这个类里面有三个主要的函数可以重载分别是:

RuntimeInitialize(LocationInfo locationInfo):初始化包含属性或字段的类的时候运行此函数,增加控制代码,可以截取到运行此属性或字段的类信息,属性类型等信息。

OnSetValue(LocationInterceptionArgs args):设置属性或字段值的时候运行此函数,增加相关设置值时代码,可以获取到此属性值、属性名等相关信息。

OnGetValue(LocationInterceptionArgs args)。获取属性或字段值的时候运行此函数。

目标类PostSharpTest:

namespace AopTest
{
    [MyLI]
    public class PostSharpTest
    {
        public int id;
        public string name;
        public PostSharpTest(int id,string name)
        {
            this.id = id;
            this.name = name;
        }
    }
}

属性、字段通知通知类MyLIAttribute:

// This file contains registration of aspects that are applied to several classes of this project.
/*AttributeTargetTypes 要拦截过滤的类
 * AttributeTargetMembers = “Dispose”,AttributeExclude = true 排除掉任何名为Dispose的方法
 * 
*AttributeTargetTypeAttributes 拦截的类属性
*AttributeTargetMemberAttributes 拦截的方法属性
*/
[assembly: AopTest.MyLI(AttributeTargetTypes = "null",
    AttributeTargetTypeAttributes = MulticastAttributes.Public,
    AttributeTargetMemberAttributes = MulticastAttributes.Public)]
namespace AopTest
{
    /* 
     * LocationInterceptionAspect类是针对属性或字段的面向方面截取。
     * 继承与它的特性将可以截取初始化属性、设置属性、获取属性等时候的数据
     * 并且可以在这几个过程中针对属性进行附加控制。
     * 这个类里面有三个主要的函数可以重载分别是
     * RuntimeInitialize(LocationInfo locationInfo)、OnSetValue(LocationInterceptionArgs args)、OnGetValue(LocationInterceptionArgs args)。他们分别意义如下:
     * RuntimeInitialize(LocationInfo locationInfo):初始化包含属性或字段的类的时候运行此函数,增加控制代码,可以截取到运行此属性或字段的类信息,属性类型等信息。
     * OnSetValue(LocationInterceptionArgs args):设置属性或字段值的时候运行此函数,增加相关设置值时代码,可以获取到此属性值、属性名等相关信息。
     * OnGetValue(LocationInterceptionArgs args)。获取属性或字段值的时候运行此函数。
     */
    [Serializable]
    public class MyLIAttribute : LocationInterceptionAspect
    {
        //当目标类初始化属性的时候运行此。
        public override void RuntimeInitialize(LocationInfo locationInfo)
        {
            Console.Write("正在初始化属性");
            //打印类名、属性或字段名、字段类型
            string name = locationInfo.DeclaringType.Name + "." +
                locationInfo.Name + " (" + locationInfo.LocationType.Name + ")"; ;
            Console.WriteLine(name);
            System.Reflection.FieldInfo finfo = locationInfo.FieldInfo;
            base.RuntimeInitialize(locationInfo);
        }
        //设置属性的时候运行
        public override void OnSetValue(LocationInterceptionArgs args)
        {
            Console.Write("正在设置属性");
            Console.WriteLine(args.LocationName+":"+ args.Value);
            base.OnSetValue(args);
        }
        //获取属性的时候运行
        public override void OnGetValue(LocationInterceptionArgs args)
        {
            Console.WriteLine("即将获取到属性");
            base.OnGetValue(args);
            Console.Write("已经获取到属性");
            Console.WriteLine(args.LocationName + ":" + args.Value);
        }
    }
}

测试方法:

 static void Main(string[] args)
        {
            PostSharpTest ps = new PostSharpTest(1, "www.b0c0.com");
            string name=ps.name;
        }

运行结果:

通过对比可以看到其实用PostSharp更加方便一点,并且没有了spring.net中aop对目标类的限制。

发表评论

电子邮件地址不会被公开。