-
Notifications
You must be signed in to change notification settings - Fork 48
方法匹配
方法匹配适用于批量应用的场景,如果将切面类型直接应用到方法上,那么将不检查任何匹配规则直接生效
所谓的粗粒度方法特征,包含以下特征:
- 方法可访问性(public或者非public)
- 是否静态方法
- 方法类型:方法 / 属性 / getter / setter / 构造方法
可以通过PointcutAttribute
进行设置,一般方法匹配使用粗粒度的方法特征匹配即可达到目的:
[Pointcut(AccessFlags.Public | AccessFlags.Method)] // 所有的public方法,不论是静态还是实例
[Pointcut(AccessFlags.Instance | AccessFlags.Property)] // 所有的实例属性(包含getter和setter),不论是静态还是实例,普通方法不会被选择
public class LoggingAttribute : MoAttribute
{
}
类 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/setter
;ctor
和cctor
分别匹配构造方法和静态构造方法;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]
,仅position
为para
时需要指定,表示参数的位置,一般为整数,从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.Ab
和a.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
类型,比如int?
即为Nullable<int>
。需要注意的是,不要将引用类型的Nullable语法也当做Nullable
类型,比如string?
其实就是string
,在Rougamo里面直接写string
,而不要写成string?
。
我们在编写C#代码时,可以直接使用括号表示ValueTuple
,在Rougamo中同样支持该比如,比如(int,string)
即表示ValueTuple<int, string>
或Tuple<int, string>
。
现在异步编程已经是基础的编程方式了,所以方法返回值为Task
或ValueTask
的方法将会非常之多,同时如果要兼容Task
和ValueTask
两种返回值,表达式还需要使用逻辑运算符||
进行连接,那将大大增加表达式的复杂性。Rougamo增加了熟悉的async
关键字用来匹配Task
和ValueTask
返回值,比如Task<int>
和ValueTask<int>
可以统一写为async int
,那么对于非泛型的Task
和ValueTask
则写为async null
。需要注意的是,目前没有单独匹配async void
的方式,void
会匹配void
和async 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...
,增加的顺序按declaringType
先method
后的顺序,详细可看后续的示例 -
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() { }
由于我们有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不互斥,两个都将被应用
}
}
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
不生效时,与它互斥的Mo2Attribute
、Mo3Attribute
都将生效。这里需要理解,定义互斥的原因是Attribute和空接口实现两种方式可能存在的重复应用,而并不是为了排除所有织入的重复。同时也不推荐使用多互斥定义,这样容易出现逻辑混乱,建议在应用织入前仔细思考一套统一的规则,而不是随意定义,然后试图使用多互斥来解决问题。