AspectJ 在APM上的应用(二)

AspectJ 语法

上篇文章介绍了 AspectJ 的基本概念,这篇文章详细分析 AspectJ 基于注解开发方式的语法。

Join Point

Join Point 表示连接点,即 AOP 可织入代码的点,下表列出了 AspectJ 的所有连接点:

Join Point 说明
Method call 方法被调用
Method execution 方法执行
Constructor call 构造函数被调用
Constructor execution 构造函数执行
Field get 读取属性
Field set 写入属性
Pre-initialization 与构造函数有关,很少用到
Initialization 与构造函数有关,很少用到
Static initialization static 块初始化
Handler 异常处理
Advice execution 所有 Advice 执行

Pointcuts

Pointcuts 是具体的切入点,可以确定具体织入代码的地方,基本的 Pointcuts 是和 Join Point 相对应的。

Join Point Pointcuts syntax
Method call call(MethodPattern)
Method execution execution(MethodPattern)
Constructor call call(ConstructorPattern)
Constructor execution execution(ConstructorPattern)
Field get get(FieldPattern)
Field set set(FieldPattern)
Pre-initialization initialization(ConstructorPattern)
Initialization preinitialization(ConstructorPattern)
Static initialization staticinitialization(TypePattern)
Handler handler(TypePattern)
Advice execution adviceexcution()

除了上面与 Join Point 对应的选择外,Pointcuts 还有其他选择方法:

Pointcuts synatx 说明
within(TypePattern) 符合 TypePattern 的代码中的 Join Point
withincode(MethodPattern) 在某些方法中的 Join Point
withincode(ConstructorPattern) 在某些构造函数中的 Join Point
cflow(Pointcut) Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,包括 P 本身
cflowbelow(Pointcut) Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,不包括 P 本身
this(Type or Id) Join Point 所属的 this 对象是否 instanceOf Type 或者 Id 的类型
target(Type or Id) Join Point 所在的对象(例如 call 或 execution 操作符应用的对象)是否 instanceOf Type 或者 Id 的类型
args(Type or Id, …) 方法或构造函数参数的类型
if(BooleanExpression) 满足表达式的 Join Point,表达式只能使用静态属性、Pointcuts 或 Advice 暴露的参数、thisJoinPoint 对象

Pointcut 表达式还可以 !、&&、|| 来组合,!Pointcut 选取不符合 Pointcut 的 Join Point,Pointcut0 && Pointcut1 选取符合 Pointcut0 和 Pointcut1 的 Join Point,Pointcut0 || Pointcut1 选取符合 Pointcut0 或 Pointcut1 的 Join Point。

上面 Pointcuts 的语法中涉及到一些 Pattern,下面是这些 Pattern 的规则,[]里的内容是可选的:

Pattern 规则
MethodPattern [!][@Annotation] [public,protected,private][static] [final] 返回值类型 [类名.]方法名(参数类型列表) [throws 异常类型]
ConstructorPattern [!][@Annotation] [public,protected,private][final] [类名.]new(参数类型列表) [throws 异常类型]
FieldPattern [!][@Annotation] [public,protected,private][static] [final] 属性类型 [类名.]属性名
TypePattern 其他 Pattern 涉及到的类型规则也是一样,可以使用 ‘!’、’‘、’..’、’+’,’!’ 表示取反,’‘ 匹配除 . 外的所有字符串,’*’ 单独使用事表示匹配任意类型,’..’ 匹配任意字符串,’..’ 单独使用时表示匹配任意长度任意类型,’+’ 匹配其自身及子类,还有一个 ‘…’表示不定个数

TypePattern 也可以使用 &&、|| 操作符,其他 Pointcut 更详细的语法说明,见官网文档 Pointcuts Language Semantics

Pointcut 示例

execution(void void android.view.View.OnClickListener+.onClick(..)) – OnClickListener 及其子类的 onClick 方法执行时

call(@retrofit2.http.GET public com.johnny.core.http..(..)) – ‘com.johnny.core.http’开头的包下面的所有 GET 方法调用时

call(android.support.v4.app.Fragment+.new(..)) – support 包中的 Fragment 及其子类的构造函数调用时

set(@Inject ) – 写入所有 @Inject 注解修饰的属性时

handler(IOException) && within(com.johnny.core.http..) – ‘com.johnny.core.http’开头的包代码中处理 IOException 时

execution(void setUserVisibleHint(..)) && target(android.support.v4.app.Fragment) && args(boolean) – 执行 Fragment 及其子类的 setUserVisibleHint(boolean) 方法时

execution(void Foo.foo(..)) && cflowbelow(execution(void Foo.foo(..))) – 执行 Foo.foo() 方法中再递归执行 Foo.foo() 时

Pointcut 声明

Pointcuts 可以在普通的 class 或 Aspect class 中定义,由 org.aspectj.lang.annotation.Pointcut 注解修饰的方法声明,方法返回值只能是 void。@Pointcut 修饰的方法只能由空的方法实现而且不能有 throws 语句,方法的参数和 pointcut 中的参数相对应。

看下面这个例子:

@Aspect
class Test {
    @Pointcut("execution(void Foo.foo(..)")
    public void executFoo() {}

    @Pointcut("executFoo() && cflowbelow(executFoo()) && target(foo) && args(i)")
    public void loopExecutFoo(Foo foo, int i) {}
}

if() 表达式

在基于 AspectJ 注解的开发方式中,if(...) 表达式的用法与其他的选择操作符不同,在 @Pointcut 的语句中 if 表达式只能是if()if(true)if(false),而且 @Pointcut 方法必须为 public static boolean,方法体内就是 if 表达式的内容,可以使用暴露的参数、静态属性、JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart。

static int COUNT = 0;

@Pointcut("call(* *.*(int)) && args(i) && if()")
public static boolean someCallWithIfTest(int i, JoinPoint jp, JoinPoint.EnclosingStaticPart esjp) {
    // any legal Java expression...
    return i > 0
        && jp.getSignature().getName.startsWith("doo")
        && esjp.getSignature().getName().startsWith("test")
        && COUNT++ < 10;
}

if() 表达式使用的比较少,大致了解下就可以了。

target() 与 this()

target() 与 this() 很容易混淆,target() 是指 Pointcut 选取的 Join Point 的所有者;this() 是指 Pointcut 选取的 Join Point 的调用的所有者。简单地说就是,PointcutA 选取的是 methodA,那么 target 就是 methodA() 这个方法的对象,而 this 就是 methodA 被调用时所在类的对象。

看下面这个例子:

class Test {
    public void test() {...}
}

class A {
    ...
    test1.test(); // test() 在 a 的某方法中调用
    ...
}

@Aspect
class TestAspect {
    @Pointcut("call(void Test.test()) && target(Test)")
    public test1() {}

    @Pointcut("call(void Test.test()) && this(A)")
    public test2() {}
}

上面代码中 test1.test() 方法属于 test1 对象,所以 target 为 test1,而该方法在 a 对象的方法中调用,所以 this 为 a。

Advice

Advice 是在切入点上织入的代码,在 AspectJ 中有五种类型:Before、After、AfterReturning、AfterThrowing、Around。

Advice 说明
@Before 在执行 Join Point 之前
@After 在执行 Join Point 之后,包括正常的 return 和 throw 异常
@AfterReturning Join Point 为方法调用且正常 return 时,不指定返回类型时匹配所有类型
@AfterThrowing Join Point 为方法调用且抛出异常时,不指定异常类型时匹配所有类型
@Around 替代 Join Point 的代码,如果要执行原来代码的话,要使用 ProceedingJoinPoint.proceed()

注意: After 和 Before 没有返回值,但是 Around 的目标是替代原 Join Point 的,所以它一般会有返回值,而且返回值的类型需要匹配被选中的 Join Point 的代码。而且不能和其他 Advice 一起使用,如果在对一个 Pointcut 声明 Around 之后还声明 Before 或者 After 则会失效。

Advice 注解修改的方法必须为 public,Before、After、AfterReturning、AfterThrowing 四种类型修饰的方法返回值也必须为 void,Advice 需要使用 JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart 时,要在方法中声明为额外的参数,@Around 方法可以使用 ProceedingJoinPoint,用以调用 proceed() 方法。

看下面几个示例,进一步了解 Advice 用法:

@Before("call(* *.*(..)) && this(foo)")
public void callFromFoo(Foo foo) {
    Log.d(TAG, "call from Foo:" + foo);
}

@AfterReturning(pointcut="call(Foo+.new(..))", returning="f")
public void itsAFoo(Foo f, JoinPoint thisJoinPoint) {
    // ...
}

@Around("call(* setAge(..)) && args(i)")
public Object twiceAsOld(int i, ProceedingJoinPoint thisJoinPoint) {
    return thisJoinPoint.proceed(new Object[]{i * 2}); // 原来参数乘以 2
}

注:Handler Pointcut 不能使用 After 和 Around。

Aspect

Aspect 就是 AOP 中的关键单位 – 切面,我们一般会把相关 Pointcut 和 Advice 放在一个 Aspect 类中,在基于 AspectJ 注解开发方式中只需要在类的头部加上 @Aspect 注解即可,@Aspect 不能修饰接口。

例如,定义一个 LogAspect,在需要的 Join Point 上加上打印日志的 Advice,这样就形成了一个 LogAspect 的切面,在编译期会将代码织入到相应的方法中,但是在编码中只需要关注 LogAspect 即可。

在多个切入点织入 Advice 代码时,会涉及到 Aspect 对象实例的问题,因为 Advice 代码是 Aspect 的方法。一般情况下,我们使用的都是单例的 Aspect,即所有 Advice 代码使用的都是同一个 Aspect 对象实例。

Singleton Aspect

文章中代码示例都是单例的 Aspect,这也是最常见的,定义方式为:@Aspect 或者 @Aspect()

编译期,ajc 编译期会给单例的切面加上静态的 aspectOf() 方法来获取单例实例,还有一个 hasAspect() 静态方法判断实例是否初始化。假设 FragmentAspect 有 Advice 方法 advice1(),织入切入点的代码就是 FragmentAspect.aspectOf().advice1()。

Per-object, Per-cflow Aspect 等

除了单例 Aspect 外,还可以根据 Join Point 的相应对象、控制流、所在类型产生不同的实例。

定义方式为:@Aspect("perthis|pertarget|percflow|percflowbelow(Pointcut) | pertypewithin(TypePattern)"),因为不常见,所以就简单介绍下,想进一步了解请看 Aspects Language Semantics

Inter-type Declarations

上面提到的都是 Pointcut 和 Advice 都是在类本身结构不变的情况下织入代码,AspectJ 的 Inter-type Declarations 可以修改类的结构,给类添加方法或者属性,让类继承多个类或者实现多个接口。但是基于 AspectJ 注解开发方式因为技术原因,目前只能让类实现多个接口,通俗的说法就是给类添加接口,也添加了接口的方法。

给类添加接口,实际通过实现了该接口的代理来完成对原类型的替换,所以需要提供实现了该接口的实现完成代理中接口的具体行为,不然只是增加接口,没有接口实现没什么用处。@DeclareMixin 就是用来确定接口的默认实现,绑定一个产生该接口的默认实现的工厂方法,以该接口为返回类型。

看下面代码,给 Fragment 添加 Title 接口:

public interface Title {
    String getTitle();
}

public class TitleImpl implements Title {
    @Override
    public String getTitle() {
        return "Test";
    }
}

@Aspect
public class FragmentAspect {
    @DeclareMixin("android.support.v4.app.Fragment")
    public static Title createDelegate() {
        return new TitleImpl();
    }
}

上面代码可以给 Fragment 添加了 Title 接口,如果@DeclareMixin("android.support.v4.app.*")的话,则给 app 下所有类添加 Title 接口,之后通过正常的类型转换来访问 Title 接口:

String title = ((Title) fragment).getTitle(); // 返回 Test 字符串

也可以将原对象作为接口默认实现的参数,这样就可以根据 fragment 的属性返回不同的 title :

public class TitleImpl implements Title {

    private final String title;

    public TitleImpl(Fragment fragment) {
        title = fragment.getClass().getSimpleName();
    }

    @Override
    public String getTitle() {
        return title;
    }
}

@Aspect
public class FragmentAspect {
    @DeclareMixin("android.support.v4.app.Fragment")
    public static Title createDelegate(Fragment fragment) {
        return new TitleImpl(fragment);
    }
}

上面代码返回 fragment 的类名作为 title。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容