Skip to content

5 Pointcut切面编程

俞正东 edited this page Sep 4, 2023 · 25 revisions

面向切面编程

我们之前介绍了利用Aspect标签来完成拦截器功能

Aspect是一对一的方式,我想要某个class开启拦截器功能我需要针对每个class去配置。 详情请点击

比如说 我有2个 controller 每个controller都有2个action方法,

    [Component]
    public class ProductController
    {
        public virtual string GetProduct(string productId)
        {
            return "GetProduct:" + productId;
        }
        
        public virtual string UpdateProduct(string productId)
        {
            return "UpdateProduct:" + productId;
        }
    }
    
    [Component]
    public class UserController
    {
        public virtual string GetUser(string userId)
        {
            return "GetUser:" + userId;
        }
        
        public virtual string DeleteUser(string userId)
        {
            return "DeleteUser:" + userId;
        }
    }

如果我需要这2个controller的action方法都在执行方法前打log 在方法执行后打log 按照上一节Aspect的话 我需要每个controller都要配置。如果我有100个controller的画我就需要配置100次,这样我觉得太麻烦了。所以我参考了Spring的Pointcut切面编程的方式实现了,下面看如何用Pointcut的方式方便的配置一种切面去适用于N个对象。

定义一个切面:创建一个class 上面打上Pointcut的标签 如下:

Pointcut标签类有如下属性:

属性名 说明
Name 名称Pointcut切面的名称(默认为空,和拦截方法进行匹配,参考下面说明)
RetType 匹配目标类的方法的返回类型(默认是%)
NameSpace 匹配目标类的namespace(默认是%)
ClassName 匹配目标类的类名称(和下面的AttributeType参数二选一必填)
AttributeType 匹配特定的标签(和上面的ClassName参数二选一必填)
AttributeFlag 当指定了AttributeType时可以用此值来扩展(具体看下面)
MethodName 匹配目标类的方法名称(默认是%)
AttributeFlag(指定按照AttributeType来匹配时生效)
  • NONE(默认值,代表打了AttributeType才匹配)
  • AssignableFrom 代表打了AttributeType或者AttributeType的父类(排除Abstract类型)的都会被匹配
  • AssignableTo 代表打了AttributeType或者AttributeType的子类的都会被匹配

切面如何匹配

image

可以看文末举例

    // *Controller 代表匹配 只要是Controller结尾的类都能匹配
    // Get* 代表上面匹配成功的类下 所以是Get打头的方法都能匹配
    [Pointcut(Class = "*Controller",Method = "Get*")]
    public class LoggerPointCut
    {
        
    }
    // *Controller 代表匹配 只要是Controller结尾的类都能匹配
    // Get* 代表上面匹配成功的类下 所以是Get打头的方法都能匹配
    [Pointcut(ClassName = "*Controller",MethodName = "Get*")]
    public class LoggerPointCut
    {
        
    }
    // 打了TestPointAttributes1注解的Component都会被识别
    [Pointcut(AttributeType = typeof(TestPointAttributes1))]
    //  因为设置了AssignableFrom 代表打了TestPointAttributes1和它的父类(排除Abstract类型)都会被识别
    //[Pointcut(AttributeType = typeof(TestPointAttributes1),AttributeFlag = AssignableFlag.AssignableFrom)]
    public class LoggerPointCut
    {
        
    }

定义好了一个Pointcut切面后 需要定义这个切面的拦截方法(也叫切入点)

配合Pointcut切面标签,可以在打了这个标签的class下定义拦截方法, 在方法上得打上特定的标签,有如下几种:

切入点 说明
Before标签 在匹配成功的类的方法执行前执行
After标签 在匹配成功的类的方法执行后执行(不管目标方法成功还是失败)
AfterReturn标签 在匹配成功的类的方法执行后执行(只是目标方法成功)
AfterThrows标签 在匹配成功的类的方法执行后执行(只是目标方法抛异常时)
Around标签 环绕目标方法,承接了匹配成功的类的方法的执行权

以上3种标签有一个可选的参数:Name (默认为空,可以和Pointcut的Name进行mapping)

  • 因为一个class上可以打多个Pointcut切面,一个Pointcut切面可以根据name去匹配对应拦截方法

切入点标签所在方法的参数说明:

  • Around切入点 必须要指定 AspectContext类型 和 AspectDelegate类型的2个参数,且返回类型要是Task 否则会报错
  • 除了Around切入点以外其他的切入点的返回值只能是Task或者Void 否则会报错
  • 除了Around切入点以外其他的切入点可以指定 AspectContext类型 参数注入进来
  • After切入点 可以指定Returing参数,可以把目标方法的返回注入进来,如果目标方法抛异常则是异常本身
  • AfterReturn切入点 可以指定Returing参数,可以把目标方法的返回注入进来
  • AfterThrows切入点 可以指定 Throwing参数,可以把目标方法抛出的异常注入进来
  • 只要你参数类型是你注册到DI容器,运行时会自动从DI容器把类型注入进来
  • 如果是指定Attribute扫描的,支持方法注入这个Attribute进来《参考这个
  • 支持方法注入当前是哪个PointCut《参考这个
  • 可以使用AutowiredValue标签来修饰参数
    /// <summary>
    /// 第一组切面
    /// </summary>
    [Pointcut(NameSpace = "Autofac.Annotation.Test.test6",Class = "Pointcut*",OrderIndex = 1)]
    public class PointcutTest1
    {
        [Around]
        public async Task Around(AspectContext context,AspectDelegate next)
        {
            Console.WriteLine("PointcutTest1.Around-start");
            await next(context);
            Console.WriteLine("PointcutTest1.Around-end");
        }

        [Before]
        public void Before()
        {
            Console.WriteLine("PointcutTest1.Before");
            
        }
        
        [After]
        public void After()
        {
            Console.WriteLine("PointcutTest1.After");
            
        }
        
        [AfterReturn(Returing = "value1")]
        public void AfterReturn(object value1)
        {
            Console.WriteLine("PointcutTest1.AfterReturn");
        }
        
        [AfterThrows(Throwing = "ex1")]
        public void Throwing(Exception ex1)
        {
            Console.WriteLine("PointcutTest1.Throwing");
        }
    }
    /// <summary>
    /// 第二组切面
    /// </summary>
    [Pointcut(NameSpace = "Autofac.Annotation.Test.test6",Class = "Pointcut*",OrderIndex = 0)]
    public class PointcutTest2
    {
        [Around]
        public async Task Around(AspectContext context,AspectDelegate next)
        {
            Console.WriteLine("PointcutTest2.Around-start");
            await next(context);
            Console.WriteLine("PointcutTest2.Around-end");
        }

        [Before]
        public void Before()
        {
            Console.WriteLine("PointcutTest2.Before");
        }
        
        [After]
        public void After()
        {
            Console.WriteLine("PointcutTest2.After");
        }
        
        [AfterReturn(Returing = "value")]
        public void AfterReturn(object value)
        {
            Console.WriteLine("PointcutTest2.AfterReturn");
        }
        
        [AfterThrows(Throwing = "ex")]
        public void Throwing(Exception ex)
        {
            Console.WriteLine("PointcutTest2.Throwing");
        }
    }
    [Component]
    public class Pointcut1Controller
    {
        //正常case
        public virtual void TestSuccess()
        {
            Console.WriteLine("Pointcut1Controller.TestSuccess");
        }
        
        //异常case
        public virtual void TestThrow()
        {
            Console.WriteLine("Pointcut1Controller.TestThrow");
            throw new ArgumentException("ddd");
        }
    }

    [Component]
    public class Pointcut2Controller
    {
        //正常case
        public virtual void TestSuccess()
        {
            Console.WriteLine("Pointcut1Controller.TestSuccess");
        }
        
        //异常case
        public virtual void TestThrow()
        {
            Console.WriteLine("Pointcut1Controller.TestThrow");
            throw new ArgumentException("ddd");
        }
    }

按照上面的配置

  • Pointcut1Controller.TestSuccess 和 TestThrow 2个方法 会被匹配
  • Pointcut2Controller.TestThrow 和 TestThrow 2个方法 会被匹配

执行顺序

单个切面顺序如下图

image

多个切面执行的顺序如下图

image

关于顺序是和上一节说的[Aspect]是一致的

如果一个方法上存在多个切面,如何排除指定切面?

[Pointcut(NameSpace = "Autofac.Annotation.Test.issue31", AttributeType = typeof(ExceptionAttIgnore1Attribute))]
public class TestIgnoreAopClass1
{
    [Before]
    public void befor()
    {
    }
}

[Pointcut(NameSpace = "Autofac.Annotation.Test.issue31", AttributeType = typeof(ExceptionAttIgnore2Attribute))]
public class TestIgnoreAopClass2
{
    [Before]
    public void befor()
    {
    }
}

public interface InterIgnoreAop2
{
    [ExceptionAttIgnore1Attribute]
    [ExceptionAttIgnore2Attribute]
    void sayIgnore();
}

[Component]
public class IgnoreAop2 : InterIgnoreAop2
{
    // 如果不打上[IgnoreAop]注解的话,那么切面TestIgnoreAopClass1和切面TestIgnoreAopClass2都会走
    [IgnoreAop] //打上的话,就都不会走了
    // [IgnoreAop(Target = new[] { typeof(TestIgnoreAopClass1)})] // 这样就只会走切面TestIgnoreAopClass2
    public virtual void sayIgnore()
    {
    }
}

AOP遇到死锁问题

想必大家都知道在winform/wpf/asp.net等有异步上下文的场景中,同步代码调用异步代码且Wait异步结果会导致死锁问题(ASP.NET Core没有此问题)。

那么该问题在AOP中会是怎样呢?

举例:在wpf中同步的click事件中调用另外一个方法,这个方法是会被AOP的

        //wpf的click事件
        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            //参考下发代码 TestController的controllerTest方法是同步方法,会被AOP
            var abd = container.Resolve<TestController>().controllerTest();
        }

    [Component]
    public class TestController
    {
        [AfterAopTestAttribute]
        public virtual string controllerTest()
        {
            Console.WriteLine(111);
            return "abc";
        }
    }
 
    public class AfterAopTestAttribute : AspectAfter
    {
        public override async Task After(AspectContext aspectContext, object result)
        {
            // 注意这里,当同步方法被aop ,当调用同步方法走到这里会被卡死
            await aspectContext.ComponentContext.Resolve<Serial>().Send();
        }
    }
    [Component]
    public class Serial
    {
        public virtual async Task Send()
        {
            await Task.Delay(100);
            Console.WriteLine("aaa");
        }
    }

image

当执行到After拦截器时候, 如果不加.ConfigureAwait(false) 会造成死锁,所以在本框架的aop执行流程里面,会自动将aop执行逻辑包裹.ConfigureAwait(false) 遇到阻塞死锁的时候会直接不等待,所以那上面代码的效果就是 await Task.Delay(100); 不会死锁,会异步出去,不等待它了

另外本框架的aop执行逻辑会自动检测死锁,比如改成如下:

    public class AfterAopTestAttribute : AspectAfter
    {
        public override async Task After(AspectContext aspectContext, object result)
        {
            // 会直接触发死锁检测,报异常出来
            aspectContext.ComponentContext.Resolve<Serial>().Send().ConfigureAwait(false).GetAwaiter().GetResult();
        }

    }

image

本框架的aop执行自带死锁检测,调试的时候可以看到错误内容,可以定位到是哪个方法造成了死锁 检测到死锁会异常终止程序 至少不会卡死!

Pointcut和Aspect同时存在

先走Pointcut的逻辑,然后再走Aspect的逻辑

注意事项

tip: 打了Pointcut的class,框架会注册为单例,不要里面定义共享属性,会有并发问题(如下图) (ARL _W P(S683%DGR))RJ0

关于字符串匹配的举例:

匹配结果 匹配模板 要匹配的字符串
匹配结果:true "%" ""
匹配结果:true "%" " "
匹配结果:true "%" "asdfa asdf asdf"
匹配结果:true "%" "%"
匹配结果:false "_" ""
匹配结果:true "_" " "
匹配结果:true "_" "4"
匹配结果:true "_" "C"
匹配结果:false "_" "CX"
匹配结果:false "[ABCD]" ""
匹配结果:true "[ABCD]" "A"
匹配结果:false "[ABCD]" "b" // 因为区分大小写
匹配结果:false "[ABCD]" "X"
匹配结果:false "[ABCD]" "AB"
匹配结果:true "[B-D]" "C"
匹配结果:true "[B-D]" "D"
匹配结果:false "[B-D]" "A"
匹配结果:false "[^B-D]" "C"
匹配结果:false "[^B-D]" "D"
匹配结果:true "[^B-D]" "A" // 不是B或者C或者D打头
匹配结果:true "[^BCD]" "A" // 不是B或者C或者D打头
匹配结果:false "[^(abc)]" "abc" //不是abc打头
匹配结果:true "[^(abc)]" "abd"
匹配结果:false "[^(abc)(def)]" "def" /不是abc或者def打头
匹配结果:false "[^(abc)(def)]" "abc"
匹配结果:true "[^(abc)(def)]" "edf"
匹配结果:true "[(abc)(def)]" "abc" // 是abc或者def打头
匹配结果:false "[(abc)]" "def"
匹配结果:true "%TEST[ABCD]XXX" "lolTESTBXXX"
匹配结果:false "%TEST[ABCD]XXX" "lolTESTZXXX"
匹配结果:false "%TEST[^ABCD]XXX" "lolTESTBXXX"
匹配结果:true "%TEST[^ABCD]XXX" "lolTESTZXXX"
匹配结果:true "%TEST[B-D]XXX" "lolTESTBXXX"
匹配结果:true "%TEST[^B-D]XXX" "lolTESTZXXX"
匹配结果:true "%Stuff.txt" "Stuff.txt"
匹配结果:true "%Stuff.txt" "MagicStuff.txt"
匹配结果:false "%Stuff.txt" "MagicStuff.txt.img"
匹配结果:false "%Stuff.txt" "Stuff.txt.img"
匹配结果:false "%Stuff.txt" "MagicStuff001.txt.img"
匹配结果:true "Stuff.txt%" "Stuff.txt"
匹配结果:false "Stuff.txt%" "MagicStuff.txt"
匹配结果:false "Stuff.txt%" "MagicStuff.txt.img"
匹配结果:true "Stuff.txt%" "Stuff.txt.img"
匹配结果:false "Stuff.txt%" "MagicStuff001.txt.img"
匹配结果:true "%Stuff.txt%" "Stuff.txt"
匹配结果:true "%Stuff.txt%" "MagicStuff.txt"
匹配结果:true "%Stuff.txt%" "MagicStuff.txt.img"
匹配结果:true "%Stuff.txt%" "Stuff.txt.img"
匹配结果:false "%Stuff.txt%" "MagicStuff001.txt.img"
匹配结果:true "%Stuff%.txt" "Stuff.txt"
匹配结果:true "%Stuff%.txt" "MagicStuff.txt"
匹配结果:false "%Stuff%.txt" "MagicStuff.txt.img"
匹配结果:false "%Stuff%.txt" "Stuff.txt.img"
匹配结果:false "%Stuff%.txt" "MagicStuff001.txt.img"
匹配结果:true "%Stuff%.txt" "MagicStuff001.txt"
匹配结果:true "Stuff%.txt%" "Stuff.txt"
匹配结果:false "Stuff%.txt%" "MagicStuff.txt"
匹配结果:false "Stuff%.txt%" "MagicStuff.txt.img"
匹配结果:true "Stuff%.txt%" "Stuff.txt.img"
匹配结果:false "Stuff%.txt%" "MagicStuff001.txt.img"
匹配结果:false "Stuff%.txt%" "MagicStuff001.txt"
匹配结果:true "%Stuff%.txt%" "Stuff.txt"
匹配结果:true "%Stuff%.txt%" "MagicStuff.txt"
匹配结果:true "%Stuff%.txt%" "MagicStuff.txt.img"
匹配结果:true "%Stuff%.txt%" "Stuff.txt.img"
匹配结果:true "%Stuff%.txt%" "MagicStuff001.txt.img"
匹配结果:true "%Stuff%.txt%" "MagicStuff001.txt"
匹配结果:true "?Stuff?.txt?" "1Stuff3.txt4"
匹配结果:false "?Stuff?.txt?" "1Stuff.txt4"
匹配结果:false "?Stuff?.txt?" "1Stuff3.txt"
匹配结果:false "?Stuff?.txt?" "Stuff3.txt4"