前言
这篇文章说的是AOP编程方式的 AspectJ 。需要了解注解相关知识juejin.cn/post/692232…
简介
AspectJ 是在静态织入代码,即在编译期注入代码的。AspectJ是一种严格意义上的AOP技术,因为它提供了完整的面向切面编程的注解,这样让使用者可以在不关心字节码原理的情况下完成代码的织入,因为编写的切面代码就是要织入的实际代码。AspectJ 以切面(aspect)为基础,切面是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关点(crosscutting concern)。
概念
- Aspect :切面,一个关注点的模块化,这个关注点可能会横切多个对象。
- Join Point :连接点,程序中可切入的点,例如方法调用时、读取某个变量时。
- Pointcut :切入点,代码注入的位置,其实就是有条件限定的 Join Point,例如只在特定方法中注入代码。
- Advice :通知,在切入点注入的代码,一般有 before、after、around 三种类型。
- Target Object :被一个或多个 aspect 横切拦截操作的目标对象。
- Weaving : 把 Advice 代码织入到目标对象的过程。
- Inter-type declarations : 用来给一个类型声明额外的方法或属性。
使用场景
AspectJ实际常见的场景大致包括
- 统计埋点
- 日志打印/打点
- 数据校验
- 行为拦截
- 性能监控
- 动态权限控制
使用方式
- 使用 AspectJ 的语言进行开发
- 通过 AspectJ 提供的注解在 Java 语言上开发 Android Studio 上一般使用注解的方式使用 AspectJ,因为 Android Studio 没有 AspectJ 插件,无法识别 AspectJ 的语法,所以使用 注解的方式使用
基本语法
Join Point
Join Point 表示连接点,即 AOP 可织入代码的点
| Join Point | 说明 |
|---|---|
| Method call | 方法被调用 |
| Method execution | 方法执行 |
| Constructor call | 构造函数被调用 |
| Constructor execution | 构造函数执行 |
| Field get | 读取属性 |
| Field set | 写入属性 |
| Static initialization | static 块初始化 |
| Handler | 异常处理 |
| Advice execution | 所有 Advice 执行 |
Pointcuts
Pointcuts 是具体的切入点,可以确定具体织入代码的地方
| 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 涉及到的类型规则也是一样,可以使用 '!'、''、'..'、'+','!' 表示取反,'' 匹配除 . 外的所有字符串,'*' 单独使用事表示匹配任意类型,'..' 匹配任意字符串,'..' 单独使用时表示匹配任意长度任意类型,'+' 匹配其自身及子类,还有一个 '...'表示不定个数 |
语法官网 www.eclipse.org/aspectj/doc…
常用的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) {}
}
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() |
fter 和 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() 方法。
Aspect
Aspect 就是 AOP 中的关键单位 -- 切面,我们一般会把相关 Pointcut 和 Advice 放在一个 Aspect 类中,在基于 AspectJ 注解开发方式中只需要在类的头部加上 @Aspect 注解即可,@Aspect 不能修饰接口。 具体使用可以试试 ,自己照着撸一套,juejin.cn/post/684490…