Skip to content

方法匹配

ihourglass edited this page Dec 16, 2024 · 3 revisions

方法匹配

方法匹配适用于批量应用的场景,如果将切面类型直接应用到方法上,那么将不检查任何匹配规则直接生效

粗粒度的方法特性匹配

所谓的粗粒度方法特征,包含以下特征:

  • 方法可访问性(public或者非public)
  • 是否静态方法
  • 方法类型:方法 / 属性 / getter / setter / 构造方法

可以通过PointcutAttribute进行设置,一般方法匹配使用粗粒度的方法特征匹配即可达到目的:

[Pointcut(AccessFlags.Public | AccessFlags.Method)] // 所有的public方法,不论是静态还是实例
[Pointcut(AccessFlags.Instance | AccessFlags.Property)] // 所有的实例属性(包含getter和setter),不论是静态还是实例,普通方法不会被选择
public class LoggingAttribute : MoAttribute
{
}

类AspectJ表达式匹配

类 AspectJ 表达式后面都称为 AspectN 表达式

AspectN 表达式匹配的方式可以使用类似 AspectJ 的表达式对方法进行匹配,匹配精度更高,也更为灵活,通过PointcutAttribute进行设置。

[Pointcut("method(* *.Get*(..))")] // 匹配所有方法名以Get开头的方法
[Pointcut("method(public static * *(..))")] // 匹配所有public静态方法
[Pointcut("getter(* *)")] // 匹配所有getter
[Pointcut("method(int[]||System.Collections.Generic.IEnumerable<int>+ *(..))")] // 匹配所有返回值是int集合的方法
public class PatternAttribute : MoAttribute
{
}

表达式支持以下几种匹配规则:

  • method([modifier] <returnType> [<declaringType>.]<methodName>([parameters]))
  • getter([modifier] <returnType> <declaringType>.<propertyName>)
  • setter([modifier] <returnType> <declaringType>.<propertyName>)
  • property([modifier] <returnType> <declaringType>.<propertyName>)
  • execution([modifier] <returnType> <declaringType>.<methodName>([parameters]))
  • ctor(<declaringType>([parameters]))
  • cctor(<declaringType>)
  • attr(<position> [index] <attributeType>)
  • regex(<regex>)

上面的规则中,getter, setter, property分别表示匹配属性的getter, setter和全部匹配(getter+setter);method表示匹配普通方法(非getter/setter/constructor);execution表示匹配所有方法,包含getter/setterctorcctor分别匹配构造方法和静态构造方法;attr表示根据Attribute进行匹配。regex是个特例,将在 正则匹配 中进行单独介绍。

上面列出的七种匹配规则,除了regex的格式特殊,其他的五种匹配规则的内容主要包含以下五个(或以下)部分:

  • [modifier],访问修饰符,可以省略,省略时表示匹配所有,访问修饰符包括以下七个:
    • private
    • internal
    • protected
    • public
    • privateprotected,即private protected
    • protectedinternal,即protected internal
    • static,需要注意的是,省略该访问修饰符表示既匹配静态也匹配实例,如果希望仅匹配实例,可以与逻辑修饰符!一起使用:!static
  • <returnType>,方法返回值类型或属性类型,类型的格式较为复杂,详见类型匹配格式
  • <declaringType>,声明该方法/属性的类的类型,类型的格式较为复杂,详见类型匹配格式
  • <methodName>/<propertyName>,方法/属性的名称,名称可以使用*进行模糊匹配,比如*Async,Get*,Get*V2等,*匹配0或多个字符
  • [parameters],方法参数列表,Rougamo的参数列表匹配相对简单,没有aspectj那么复杂,仅支持任意匹配和全匹配
    • 使用..表示匹配任意参数,这里说的任意是指任意多个任意类型的参数
    • 如果不进行任意匹配,那么就需要指定参数的个数及类型,当然类型是按照类型匹配格式进行匹配的
  • <position>,Attribute的位置,可选位置如下:
    • type,Attribute应用于类型上
    • exec,Attribute应用于方法/属性/属性getter/属性setter上
    • para,Attribute应用于参数上,para需要与[index]配合使用,请看后续的介绍
    • ret,Attribute应用于返回值上
    • *,使用*表示匹配任意位置
  • [index],仅positionpara时需要指定,表示参数的位置,一般为整数,从0开始表示第一个参数,如果使用*表示任意参数位置

类型匹配格式

在肉夹馍中,一个类型的字符表达形式为全命名空间.类型名称,类型表达式匹配的就是这个签名。

嵌套类

嵌套类虽然使用不多,但该支持的还是要支持到。Rougamo使用/作为嵌套类连接符,这里与平时编程习惯里的连接符+不一致,主要是考虑到+是一个特殊字符,表示子类,为了方便阅读,所以采用了另一个符号。比如a.b.c.D/E就表示命名空间为a.b.c,外层类为D的嵌套类E。当然嵌套类支持多层嵌套。

泛型

需要首先声明的是,泛型和static一样,在不声明时匹配全部,也就是既匹配非泛型类型也匹配泛型类型,如果希望仅匹配非泛型类型或仅匹配泛型类型时需要额外定义,泛型的相关定义使用<>表示。

  • 仅匹配非泛型类型:a.b.C<!>,使用逻辑非!表示不匹配任何泛型
  • 匹配任意泛型:a.b.C<..>,使用两个点..表示匹配任意多个任意类型的泛型
  • 匹配指定数量任意类型泛型:a.b.C<,,>,示例表示匹配三个任意类型泛型,每添加一个,表示额外匹配一个任意类型的泛型,你可能已经想到了a.b.C<>表示匹配一个任意类型的泛型
  • 开放式与封闭式泛型类型:未确定泛型类型的称为开放式泛型类型,比如List<T>,确定了泛型类型的称为封闭式泛型类型,比如List<int>,那么在编写匹配表达式时,如果希望指定具体的泛型,而不是像上面介绍的那种任意匹配,那么对于开放式未确定的泛型类型,可以使用我们常用的T1,T2,TA,TX等表示,对于封闭式确定的泛型类型直接使用确定的类型即可。
    // 比如我们有如下泛型类型
    public class Generic<T1, T2>
    {
        public static void M(T1 t1, int x, T2 t2) { }
    }
    
    // 定义匹配表达式时,对于开放式泛型类型,并不需要与类型定义的泛型名称一致,比如上面叫T1,T2,表达式里用TA,TB
    [Pointcut("method(* *<TA,TB>.*(TA,int,TB))")]
    public class TestAttribute : MoAttribute
    {
    }
  • 泛型方法:除了类可以定义泛型参数,方法也可以定义泛型参数,方法的泛型参数与类型的泛型参数使用方法一致,就不再额外介绍了
    // 比如我们有如下泛型类型
    public class Generic<T1, T2>
    {
        public static void M<T3, T4>(T1 t1, T2 t2, T3 t3, T4 t4) { }
    }
    
    // 定义匹配表达式时,对于开放式泛型类型,并不需要与类型定义的泛型名称一致,比如上面叫T1,T2,表达式里用TA,TB
    [Pointcut("method(* *<TA,TB>.*<TX, TY>(TA,TB,TX,TY))")]
    // 同样可以使用非泛型匹配、任意匹配和任意类型匹配
    // [Pointcut("method(* *<TA,TB>.*<..>(TA,TB,*,*))")]
    public class TestAttribute : MoAttribute
    {
    }
模糊匹配

在前面介绍过两种模糊匹配,一种是名称模糊匹配*,一种是参数/泛型任意匹配..。在类型的模糊匹配上依旧使用的是这两个符号。

类型格式由两部分组成命名空间.类型名称,所以类型的模糊匹配可以分为:命名空间匹配、类型名称匹配、泛型匹配、子类匹配,其中泛型匹配在上一节刚介绍过,子类匹配将在下一节介绍,本节主要讲述类型基本的模糊匹配规则。

  • 类型名称匹配:类型名称的模糊匹配很简单,可以使用*匹配0或多个字符,比如*Service,Mock*,Next*Repo*V2等。需要注意的是,*并不能直接匹配任意嵌套类型,比如期望使用*Service*来匹配AbcService+Xyz是不可行的,嵌套类型需要明确指出,比如*Service/*,匹配名称以Service结尾的类型的嵌套类,如果是二层嵌套类,也需要明确指出*Service/*/*
  • 命名空间匹配
    • 缺省匹配:在命名空间缺省的情况下表示匹配任意命名空间,也就是只要类型名称即可,比如表达式Abc可以匹配l.m.n.Abc也可以匹配x.y.z.Abc
    • 完全匹配:不使用任何通配符,编写完全的命名空间,即可进行完全匹配
    • 名称模糊:命名空间有一或多段,每一段之间用.连接,和类型名称匹配一样,每一段的字符都可以使用*自行匹配,比如*.x*z.ab*.vv
    • 多段模糊:使用..可以匹配0或多段命名空间,比如*..xyz.Abc可以匹配a.b.xyz.Abc也可以匹配lmn.xyz.Abc..也可以多次使用,比如使用a..internal..t*..Ab匹配a.internal.tk.Aba.b.internal.c.t.u.Ab
子类匹配

在肉夹馍中使用+表示进行子类匹配,比如IService+表示匹配实现了IService接口的所有类型。另外子类匹配还可以与通配符一起使用,比如method(* *(*Provider+))表示匹配方法参数仅一个且参数类型是以Provider结尾的类型的子类。

语法糖

基础类型

对于常用基础类型,Rougamo支持类型简写,让表达式看起来更简洁清晰。目前支持简写的类型有bool, byte, short, int, long, sbyte, ushort, uint, ulong, char, string, float, double, decimal, object, void

Nullable

正如我们平时编程一样,我们可以使用?表示Nullable类型,比如int?即为Nullable<int>。需要注意的是,不要将引用类型的Nullable语法也当做Nullable类型,比如string?其实就是string,在Rougamo里面直接写string,而不要写成string?

ValueTuple

我们在编写C#代码时,可以直接使用括号表示ValueTuple,在Rougamo中同样支持该比如,比如(int,string)即表示ValueTuple<int, string>Tuple<int, string>

Task

现在异步编程已经是基础的编程方式了,所以方法返回值为TaskValueTask的方法将会非常之多,同时如果要兼容TaskValueTask两种返回值,表达式还需要使用逻辑运算符||进行连接,那将大大增加表达式的复杂性。Rougamo增加了熟悉的async关键字用来匹配TaskValueTask返回值,比如Task<int>ValueTask<int>可以统一写为async int,那么对于非泛型的TaskValueTask则写为async null。需要注意的是,目前没有单独匹配async void的方式,void会匹配voidasync void

类型及方法

前面有介绍到,类型的表达由命名空间.类型名称组成,如果我们希望匹配任意类型时,标准的写法应该是*..*,其中*..表示任意命名空间,后面的*表示任意类型名称,对于任意类型,我们可以简写为*。同样的,任意类型的任意方法的标准写法应该是*..*.*,其中前面的*..*表示任意类型,之后的.是连字符,最后的*表示任意方法,这种我们同样可以简写为*。所以method(*..* *..*.*(..))method(* *(..))表达的意思相同。

正则匹配

对于每个方法,Rougamo都会为其生成一个字符串签名,正则匹配即是对这串签名的正则匹配。其签名格式与method/execution的格式类似modifiers returnType declaringType.methodName([parameters])

  • modifiers包含两部分,一部分是可访问性修饰符,即private/protected/internal/public/privateprotected/protectedinternal,另一部分是是否静态方法static,非静态方法省略static关键字,两部分中间用空格分隔。
  • returnType/declaringType均为命名空间.类型名称的全写,需要注意的是,在正则匹配的签名中所有的类型都是全名称,不可使用类似int去匹配System.Int32
  • 泛型,类型和方法都可能包含泛型,对于封闭式泛型类型,直接使用类型全名称即可;对于开放式泛型类型,我们遵守以下的规定,泛型从T1开始向后增加,即T1/T2/T3...,增加的顺序按declaringTypemethod后的顺序,详细可看后续的示例
  • parameters,参数按每个参数的全名称展开即可
  • 嵌套类型,嵌套类型使用/连接
namespace a.b.c;

public class Xyz
{
    // 签名:public System.Int32 a.b.c.Xyz.M1(System.String)
    public int M1(string s) => default;

    // 签名:public static System.Void a.b.c.Xyz.M2<T1>(T1)
    public static void M2<T>(T value) { }

    public class Lmn<TU, TV>
    {
        // 签名:internal System.Threading.Tasks.Task<System.DateTime> a.b.c.Xyz/Lmn<T1,T2>.M3<T3,T4>(T1,T2,T3,T4)
        internal Task<DateTime> M3<TO, TP>(TU u, TV v, TO o, TP p) => Task.FromResult(DateTime.Now);

        // 签名:private static System.Threading.Tasks.ValueTask a.b.c.Xyz/Lmn<T1,T2>.M4()
        private static async ValueTask M4() => await Task.Yeild();
    }
}

正则匹配存在编写复杂的问题,同时也不支持子类匹配,所以一般不编写正则匹配规则,其主要是作为其他匹配规则的一种补充,可以支持一些更为复杂的名称匹配。由于Rougamo支持逻辑运算法,所以也给到正则更多辅助的空间,比如我们想要查找方法名不以Async结尾的Task/ValueTask返回值方法method(async null *(..)) && regex(^\S+ (static )?\S+ \S+?(?<!Async)\()

忽略编织

Rougamo是具有批量织入能力的,虽然目前版本提供了较为丰富的匹配规则,但是如果为了排除某个或某几个方法而让匹配表达式过于复杂,那就得不偿失了。对于排除特定某个或几个方法时,可以直接使用IgnoreMoAttribute,将其应用到方法上就是该方法忽略织入,应用到类上就是该类的所有方法忽略织入,应用到程序集上就是整个程序集的所有方法忽略织入。

另外,忽略织入时可以指定忽略的类型,比如一个方法应用了多个MoAttribute,可以单独忽略某一个,在不指定时表示忽略全部。

// 当前程序集忽略所有织入
[assembly: IgnoreMo]
// 当前程序集忽略TheMoAttribute的织入
[assembly: IgnoreMo(MoTypes = new[] { typeof(TheMoAttribute))]

// 当前类忽略所有织入
[IgnoreMo]
class Class1
{
    // ...
}

// 当前类忽略TheMoAttribute的织入
[IgnoreMo(MoTypes = new[] { typeof(TheMoAttribute))]
class Class2
{
    // ...
}

切面类型去重

默认去重

当一个方法上应用了多个肉夹馍切面类型时,且具有相同的类型、相同的构造参数(Attribute传入)、相同的属性参数(Attribute传入),那么肉夹馍就会认为这些切面类型时完全相同的,将只有一个会生效。

[Test]
[Test] // 与上一个重复,将只有一个生效
[Test(1)]
[Test(1)] // 与上一个重复,将只有一个生效
[Test(3, X = typeof(Y))]
[Test(3, X = typeof(Y))] // 与上一个重复,将只有一个生效
void M() { }

互斥去重

单类型互斥(IRougamo<,>)

由于我们有Attribute标记和接口实现两种织入方式,那么就可能出现同时应用的情况,而如果两种织入的内容是相同的,那就会出现重复织入的情况,为了尽量避免这种情况,在接口定义时,可以定义互斥类型,也就是同时只有一个能生效,具体哪个生效,根据优先级来定。

public class Mo1Attribute : MoAttribute
{
    // ...
}
public class Mo2Attribute : MoAttribute
{
    // ...
}
public class Mo3Attribute : MoAttribute
{
    // ...
}

public class Test : IRougamo<Mo1Attribute, Mo2Attribute>
{
    [Mo2]
    public void M1()
    {
        // Mo2Attribute应用于方法上,优先级高于接口实现的Mo1Attribute,Mo2Attribute将被应用
    }

    [Mo3]
    public void M2()
    {
        // Mo1Attribute和Mo3Attribute不互斥,两个都将被应用
    }
}

多类型互斥(IRepulsionsRougamo<,>)

IRougamo<,>只能与一个类型互斥,IRepulsionsRougamo<,>则可以与多个类型互斥

public class Mo1Attribute : MoAttribute
{
}
public class Mo2Attribute : MoAttribute
{
}
public class Mo3Attribute : MoAttribute
{
}
public class Mo4Attribute : MoAttribute
{
}
public class Mo5Attribute : MoAttribute
{
}

public class TestRepulsion : MoRepulsion
{
    public override Type[] Repulsions => new[] { typeof(Mo2Attribute), typeof(Mo3Attribute) };
}

[assembly: Mo2]
[assembly: Mo5]

public class Class2 : IRepulsionsRougamo<Mo1Attribute, TestRepulsion>
{
    [Mo3]
    public void M1()
    {
        // Mo1与Mo2、Mo3互斥,但由于Mo3优先级高于Mo1,所以Mo1不生效时,所有互斥类型都将生效
        // 所以最终Mo2Attribute、Mo3Attribute、Mo5Attribute将被应用
        Console.WriteLine("m1");
    }

    [Mo4]
    public void M2()
    {
        // Mo1与Mo2、Mo3互斥,但由于Mo1优先级高于Mo2,所以Mo2将不生效
        // 最终Mo1Attribute、Mo4Attribute、Mo5Attribute将被应用
        Console.WriteLine("m2");
    }
}

通过上面的例子,你可能注意到,这个多类型互斥并不是多类型之间互相互斥,而是第一个泛型与第二个泛型定义的类型互斥,第二个泛型之间并不互斥,也就像上面的示例那样,当Mo1Attribute不生效时,与它互斥的Mo2AttributeMo3Attribute都将生效。这里需要理解,定义互斥的原因是Attribute和空接口实现两种方式可能存在的重复应用,而并不是为了排除所有织入的重复。同时也不推荐使用多互斥定义,这样容易出现逻辑混乱,建议在应用织入前仔细思考一套统一的规则,而不是随意定义,然后试图使用多互斥来解决问题。

Clone this wiki locally