AspectJ in Android 系列:
AspectJ in Android (一),AspectJ 基础概念
AspectJ in Android (二),AspectJ 语法
AspectJ in Android (三),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。
关于 AspectJ 基于注解开发方式的语法就讲到这里,下一篇文章根据实际例子介绍 AspectJ 的常见用法。
参考资料