×

AspectJ in Android (二),AspectJ 语法

96
JohnnyShieh
2017.09.15 14:50* 字数 1896

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 的常见用法。

参考资料

Android
Web note ad 1