AspectJ 是 Android 平臺(tái)上一種比較高效和簡(jiǎn)單的實(shí)現(xiàn) AOP 技術(shù)的方案。
相類似的方案有以下幾種:
AspectJ: 一個(gè) JavaTM 語(yǔ)言的面向切面編程的無(wú)縫擴(kuò)展(適用Android)。
Javassist for Android :用于字節(jié)碼操作的知名 java 類庫(kù) Javassist 的 Android 平臺(tái)移植版。
DexMaker :Dalvik 虛擬機(jī)上,在編譯期或者運(yùn)行時(shí)生成代碼的 Java API。
ASMDEX :一個(gè)類似 ASM 的字節(jié)碼操作庫(kù),運(yùn)行在Android平臺(tái),操作Dex字節(jié)碼。
AOP 是什么?
在軟件業(yè),AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是OOP的延續(xù),是軟件開發(fā)中的一個(gè)熱點(diǎn),也是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。
------以上解釋來(lái)自百度百科。
簡(jiǎn)單的來(lái)講,AOP是一種:可以在不改變?cè)瓉?lái)代碼的基礎(chǔ)上,通過(guò)“動(dòng)態(tài)注入”代碼,來(lái)改變?cè)瓉?lái)執(zhí)行結(jié)果的技術(shù)。
AOP 能做什么?
- 日志
- 持久化
- 性能監(jiān)控
- 數(shù)據(jù)校驗(yàn)
- 緩存
- 其他更多
AspectJ 術(shù)語(yǔ)
- JPoint:代碼可注入的點(diǎn),比如一個(gè)方法的調(diào)用處或者方法內(nèi)部、“讀、寫”變量等。
- Pointcut:用來(lái)描述 JPoint 注入點(diǎn)的一段表達(dá)式,比如:調(diào)用 Animal 類 fly 方法的地方,call(* Animal.fly(..))。
- Advice:常見的有 Before、After、Around 等,表示代碼執(zhí)行前、執(zhí)行后、替換目標(biāo)代碼,也就是在 Pointcut 何處注入代碼。
- Aspect:Pointcut 和 Advice 合在一起稱作 Aspect。
引入 AspectJ
app/build.grade加入以下配置項(xiàng):
...
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.1'
}
}
repositories {
mavenCentral()
}
android {
...
}
dependencies {
...
compile 'org.aspectj:aspectjrt:1.8.1'
}
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.5",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
AspectJ 語(yǔ)法
AspectJ 的使用相對(duì)來(lái)說(shuō)還是有點(diǎn)復(fù)雜,所以我整理了一份參考手冊(cè)來(lái)方便查閱,以下是貼圖,為了方便使用,文章最后會(huì)給出 pdf 版的下載地址。
JPoint 的分類和對(duì)應(yīng)的 Pointcut 如下:

Pointcut 中的 Signature 參考:

以上的 Signature 都是由一段表達(dá)式組成,且每個(gè)關(guān)鍵詞之間都有“空格”,下面是對(duì)關(guān)鍵詞的解釋:

Pointcut 語(yǔ)法熟悉了之后,Advice 就顯得很簡(jiǎn)單了,它包含以下幾個(gè):


有了以上參考手冊(cè),我們來(lái)看看如何使用吧~
開始使用
一、Method -> call
定義一個(gè) Animal 類,包含一個(gè) fly 方法:
public class Animal {
private static final String TAG = "Animal";
public void fly() {
Log.e(TAG, this.toString() + "#fly");
}
}
在調(diào)用 fly 的地方,之前插入一段代碼:
@Aspect
public class MethodAspect {
private static final String TAG = "ConstructorAspect";
@Pointcut("call(* android.aspectjdemo.animal.Animal.fly(..))")
public void callMethod() {}
@Before("callMethod()")
public void beforeMethodCall(JoinPoint joinPoint) {
Log.e(TAG, "before->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
}
}
使用 @Pointcut 來(lái)注解方法,定義具體的 Pointcut ,call(MethodSignature) 關(guān)鍵字表示方法被調(diào)用,MethodSignature 的定義參考 1-2、1-3表格。

調(diào)用 fly 之前插入一段代碼,所以 Advice 需要使用 @Before,@Before 的參數(shù)就是 使用@Pointcut 注解的方法名稱。
當(dāng)然,你也可以合并 Pointcut 和 Advice,像這樣:
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect";
@Before("call(* android.aspectjdemo.animal.Animal.fly(..))")
public void beforeMethodCall(JoinPoint joinPoint) {
Log.e(TAG, "before->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
}
}
最后,就是在 MethodAspect 加上 @Aspect 注解,這樣 AspectJ 在編譯時(shí)會(huì)查找被 @Aspect 注解的 class,然后 AOP 的過(guò)程會(huì)自動(dòng)完成。
編譯運(yùn)行之后,可以在 app/build/intermediates/classes/debug 目錄查看編譯后的 class 文件:
...
Animal animal = new Animal();
JoinPoint var5 = Factory.makeJP(ajc$tjp_0, this, animal);
MethodAspect.aspectOf().beforeMethodCall(var5);
animal.fly();
...
animal.fly() 之前插入了一段代碼,調(diào)用就是 MethodAspect#beforeMethodCall 方法,查看 logcat 輸出:

如果使用 @After 類型的 Advice,則會(huì)在animal.fly() 之后插入:
...
Animal animal = new Animal();
Animal var6 = animal;
JoinPoint var5 = Factory.makeJP(ajc$tjp_0, this, animal);
try {
var6.fly();
} catch (Throwable var9) {
MethodAspect.aspectOf().afterMethodCall(var5);
throw var9;
}
MethodAspect.aspectOf().afterMethodCall(var5);
...
我們來(lái)看一下 @Around 類型的 Advice,它會(huì)替換原先的執(zhí)行代碼:
/**
* 不能和Before、After一起使用
* @param joinPoint
* @throws Throwable
*/
@Around("call(* android.aspectjdemo.animal.Animal.fly(..))")
public void aroundMethodCall(ProceedingJoinPoint joinPoint) throws Throwable {
Log.e(TAG, "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
// 執(zhí)行原代碼
joinPoint.proceed();
}
@Around 不能和 @Before、@After 一起使用,如果不小心這樣用了,你會(huì)發(fā)現(xiàn)沒(méi)有任何效果。
@Around 會(huì)替換原先執(zhí)行的代碼,但如果你仍然希望執(zhí)行原先的代碼,可以使用joinPoint.proceed()。
二、Method -> execution
與 call 類似,只不過(guò)執(zhí)行點(diǎn)(JPoint)在方法內(nèi)部,比如:我們希望在 fly 方法的Log.e(TAG, this.toString() + "#fly")這段代碼執(zhí)行前,插入一段代碼:
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect";
@Before("execution(* android.aspectjdemo.animal.Animal.fly(..))")
public void beforeMethodExecution(JoinPoint joinPoint) {
Log.e(TAG, "before->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
}
}
execution 關(guān)鍵字表示方法執(zhí)行內(nèi)部,編譯后 class 文件如下:
public void fly() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_1, this, this);
MethodAspect.aspectOf().beforeMethodExecution(var1);
Log.e("Animal", this.toString() + "#fly");
}
logcat 輸出如下:

@After 和 @Around 這里就不做演示了,和 Method -> call 中講解的類似,只不過(guò)是在 fly 方法內(nèi)部。
三、Constructor -> call & execution
Constructor 和 Method 幾乎一模一樣,最大的區(qū)別就在 Signature,如下表:

Constructor 沒(méi)有返回值類型,且函數(shù)名只能是 new,一個(gè)示例如下:
@Aspect
public class ConstructorAspect {
private static final String TAG = "ConstructorAspect";
@Before("execution(android.aspectjdemo.animal.Animal.new(..))")
public void beforeConstructorExecution(JoinPoint joinPoint) {
Log.e(TAG, "before->" + joinPoint.getThis().toString() + "#" + joinPoint.getSignature().getName());
}
}
四、Field -> get
語(yǔ)法概要:

Animal 包含年齡age屬性、返回age的getAge方法:
public class Animal {
private static final String TAG = "Animal";
private int age;
public Animal() {
this.age = 10;
}
public int getAge() {
Log.e(TAG, "getAge: ");
return this.age;
}
}
比如,我們希望不管怎么修改age的值,最后獲取的age都為100,那么就需要替換訪問(wèn)age的代碼:
@Aspect
public class FieldAspect {
private static final String TAG = "FieldAspect";
@Around("get(int android.aspectjdemo.animal.Animal.age)")
public int aroundFieldGet(ProceedingJoinPoint joinPoint) throws Throwable {
// 執(zhí)行原代碼
Object obj = joinPoint.proceed();
int age = Integer.parseInt(obj.toString());
Log.e(TAG, "age: " + age);
return 100;
}
}
這樣,animal.getAge()返回的值一定是100:
Animal animal = new Animal();
int age = animal.getAge();
Log.e(TAG, "true age: " + age);
logcat 輸出如下:

同時(shí)看下編譯后的class:
public int getAge() {
Log.e(TAG, "getAge: ");
JoinPoint var2 = Factory.makeJP(ajc$tjp_2, this, this);
FieldAspect var10000 = FieldAspect.aspectOf();
Object[] var3 = new Object[]{this, this, var2};
return var10000.aroundFieldGet((new Animal$AjcClosure3(var3)).linkClosureAndJoinPoint(4112));
}
可以看到,原先的this.age已經(jīng)被替換成調(diào)用FieldAspect#aroundFieldGet方法了。
五、Field -> set & withincode
與 get 對(duì)應(yīng)的是 set:表示修改某個(gè)屬性,比如setAge方法中的this.age = age:
public class Animal {
private static final String TAG = "Animal";
private int age;
public Animal() {
this.age = 10;
}
public void setAge(int age) {
Log.e(TAG, "setAge: ");
this.age = age;
}
}
假如我們希望替換這段代碼,讓調(diào)用方無(wú)法改變age:
@Aspect
public class FieldAspect {
private static final String TAG = "FieldAspect";
@Around("set(int android.aspectjdemo.animal.Animal.age)")
public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {
Log.e(TAG, "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
}
}
編譯后的class:
public class Animal {
private static final String TAG = "Animal";
private int age;
public Animal() {
Log.e("Animal", "Animal構(gòu)造函數(shù)");
byte var1 = 10;
JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, Conversions.intObject(var1));
FieldAspect var10000 = FieldAspect.aspectOf();
Object[] var4 = new Object[]{this, this, Conversions.intObject(var1), var3};
var10000.aroundFieldSet((new Animal$AjcClosure1(var4)).linkClosureAndJoinPoint(4112));
}
public void setAge(int age) {
Log.e("Animal", "setAge: ");
JoinPoint var4 = Factory.makeJP(ajc$tjp_3, this, this, Conversions.intObject(age));
FieldAspect var10000 = FieldAspect.aspectOf();
Object[] var5 = new Object[]{this, this, Conversions.intObject(age), var4};
var10000.aroundFieldSet((new Animal$AjcClosure5(var5)).linkClosureAndJoinPoint(4112));
}
}
我們發(fā)現(xiàn),setAge方法中的this.age = age的確被替換了,但是原先構(gòu)造函數(shù)初始化age的代碼:this.age = 10也被替換了。
這時(shí)候,就需要使用withincode關(guān)鍵字了。
六、withincode
語(yǔ)法概要:

比如,我們要排除 Animal 的構(gòu)造函數(shù)修改age的 JPoint,可以這樣寫:
@Aspect
public class FieldAspect {
private static final String TAG = "FieldAspect";
@Around("set(int android.aspectjdemo.animal.Animal.age) && !withincode(android.aspectjdemo.animal..*.new(..))")
public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {
Log.e(TAG, "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
}
}
Pointcut 多個(gè)條件使用&&、||運(yùn)算符連接,!表示否的意思。
再看一下編譯后的class,構(gòu)造函數(shù)中的 JPoint 已經(jīng)被排除:
public class Animal {
private static final String TAG = "Animal";
private int age;
public Animal() {
Log.e("Animal", "Animal構(gòu)造函數(shù)");
this.age = 10;
}
public void setAge(int age) {
Log.e("Animal", "setAge: ");
JoinPoint var4 = Factory.makeJP(ajc$tjp_3, this, this, Conversions.intObject(age));
FieldAspect var10000 = FieldAspect.aspectOf();
Object[] var5 = new Object[]{this, this, Conversions.intObject(age), var4};
var10000.aroundFieldSet((new Animal$AjcClosure5(var5)).linkClosureAndJoinPoint(4112));
}
}
七、staticinitialization
語(yǔ)法概要:

TypeSignature 語(yǔ)法:

JPoint 為static塊初始化內(nèi):
public class Animal {
private static final String TAG = "Animal";
static {
Log.e(TAG, "static block");
}
}
如果要在static塊初始化之前,插入代碼:
@Aspect
public class StaticInitializationAspect {
private static final String TAG = "StaticAspect";
@Before("staticinitialization(android.aspectjdemo.animal.Animal)")
public void beforeStaticBlock(JoinPoint joinPoint) {
Log.d(TAG, "beforeStaticBlock: ");
}
}
編譯后的class:
public class Animal {
private static final String TAG = "Animal";
static {
ajc$preClinit();
JoinPoint var0 = Factory.makeJP(ajc$tjp_3, (Object)null, (Object)null);
StaticInitializationAspect.aspectOf().beforeStaticBlock(var0);
Log.e("Animal", "static block");
}
}
八、handler
用來(lái)匹配 catch 的異常,比如 Animal 的 hurt 方法:
public class Animal {
public void hurt(){
try {
int i = 4 / 0;
} catch (ArithmeticException e) {
e.printStackTrace();
}
}
}
如果我們需要統(tǒng)計(jì)所有出現(xiàn)ArithmeticException的點(diǎn),則可以使用 handler:
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect";
/**
* handler
* 不支持@After、@Around
*/
@Before("handler(java.lang.ArithmeticException)")
public void handler() {
Log.e(TAG, "handler");
}
}
注意 handler 不支持 @After 與 @Around,且異常只支持編譯時(shí)匹配,也就是handler(java.lang.Exception)無(wú)法匹配java.lang.ArithmeticException,雖然ArithmeticException繼承自Exception。
九、Advice 之 @AfterThrowing
@AfterThrowing 屬于 @After 的變種,方法的結(jié)束包括兩種狀態(tài):正常結(jié)束和異常退出。
我們經(jīng)常需要收集拋出異常的方法信息,這時(shí)候可以使用 @AfterThrowing。
比如 Animal 的hurtThrows會(huì)拋出java.lang.ArithmeticException異常:
public class Animal {
public void hurtThrows(){
int i = 4 / 0;
}
}
我們可以這樣收集異常:
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect";
@AfterThrowing(pointcut = "call(* *..*(..))", throwing = "throwable")
public void anyFuncThrows(Throwable throwable) {
Log.e(TAG, "hurtThrows: ", throwable);
}
}
call(* *..*(..))表示任意類的任意方法,被調(diào)用的 JPoint。
throwing = "throwable"描述了異常參數(shù)的名稱,也就是anyFuncThrows方法中的參數(shù)throwable。
你可以通過(guò) Throwable 收集方法調(diào)用棧的信息,這里就不做過(guò)多講解了。
這里,需要強(qiáng)調(diào)幾點(diǎn):
1、@AfterThrowing 不支持 Field -> get & set,一般用在 Method 和 Constructor,其他暫時(shí)沒(méi)測(cè)試過(guò);
2、捕獲的是拋出異常的方法,即使這個(gè)方法的調(diào)用方已經(jīng)處理了此異常,比如:
try {
animal.hurtThrows();
} catch (Exception e) {}
即使這樣,MethodAspect#anyFuncThrows也會(huì)被觸發(fā)。
接下來(lái),我們看下方法正常結(jié)束的情況。
十、Advice 之 @AfterReturning & args
這里講的正常結(jié)束,指的是有返回值的方法。
假如 Animal 有兩個(gè)getHeight方法:
public class Animal {
public int getHeight() {
return 0;
}
public int getHeight(int sex) {
switch (sex) {
case 0:
return 163;
case 1:
return 173;
}
return 173;
}
}
我們想要拿到 getHeight 的返回值,做一些其他事情(比如,數(shù)據(jù)統(tǒng)計(jì)、緩存等),可以這樣做:
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect";
@AfterReturning(pointcut = "execution(* android.aspectjdemo.animal.Animal.getHeight(..))", returning = "height")
public void getHeight(int height) {
Log.d(TAG, "getHeight: " + height);
}
}
如果你調(diào)用animal.getHeight(),此方法會(huì)得到0;
如果你調(diào)用animal.getHeight(0),此方法會(huì)得到163。
如果你只對(duì)getHeight(int sex)感興趣,有兩種做法:
1、Pointcut 中表示任意參數(shù)的 .. 改為 int
@AfterReturning(pointcut = "execution(* android.aspectjdemo.animal.Animal.getHeight(int))", returning = "height")
2、 && args(int)
@AfterReturning(pointcut = "execution(* android.aspectjdemo.animal.Animal.getHeight(..)) && args(int)", returning = "height")

舉個(gè)高級(jí)栗子
這里介紹一個(gè)在實(shí)際項(xiàng)目中,經(jīng)常碰到的一個(gè)問(wèn)題:Android M 6.0+ 之后危險(xiǎn)權(quán)限需要?jiǎng)討B(tài)申請(qǐng)。
如果有一些老的項(xiàng)目需要適配,一般做法是去修改原有的代碼,比如我們有一個(gè)啟動(dòng)相機(jī)拍照的方法:
public void camera() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(getExternalCacheDir() + "photo.jpg")));
startActivity(intent);
}
可能需要這么改:
Utils.requestPermisson(this, Manifest.permission.CAMERA).callback(new Callback(){
public void onGranted(){
camera();
}
public void onDenied() {}
});
如果你封裝了請(qǐng)求權(quán)限工具類,這樣改看起來(lái)也沒(méi)什么問(wèn)題,無(wú)非就是把所有類似的地方都加上這個(gè)段申請(qǐng)權(quán)限的代碼。
如果沒(méi)有封裝,只能是更痛苦。如果使用 AspectJ,可以通過(guò)一行注解,解決所有需要需要申請(qǐng)權(quán)限的方法。
一、定義注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MPermisson {
String value();
}
value表示要申請(qǐng)的權(quán)限名稱,比如Manifest.permission.CAMERA。
二、編寫 Aspect
@Aspect
public class PermissonAspect {
@Around("execution(@android.aspectjdemo.MPermisson * *(..)) && @annotation(permisson)")
public void checkPermisson(final ProceedingJoinPoint joinPoint, MPermisson permisson) throws Throwable {
// 權(quán)限
String permissonStr = permisson.value();
// 正常需要使用維護(hù)的棧頂Activity作為上下文,這里為了演示需要
MainActivity mainActivity = (MainActivity) joinPoint.getThis(); // 權(quán)限申請(qǐng)
Utils.requestPermisson(mainActivity, Manifest.permission.CAMERA).callback(new Callback(){
public void onGranted(){
try {
// 繼續(xù)執(zhí)行原方法
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
public void onDenied() {}
});
}
}
@annotation(permisson)用來(lái)表示permisson參數(shù)是注解類型。
三、使用 @MPermisson
在需要申請(qǐng)權(quán)限的方法上加上@MPermisson注解,其它代碼不用做修改:
@MPermisson(value = Manifest.permission.CAMERA)
public void camera() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(getExternalCacheDir() + "photo.jpg")));
startActivity(intent);
}
如果你項(xiàng)目中有其他地方,也需要申請(qǐng)權(quán)限,只需要在涉及到權(quán)限的方法上加上@MPermisson(value = "你的權(quán)限")即可。
寫在最后
以上介紹的內(nèi)容,基本上已經(jīng)涵蓋了 AspectJ 所有常用的方法。
剩余的幾個(gè)用法,比如adviceexecution()、within(TypePattern)、cflow(pointcuts)、cflowbelow(pointcuts)、this(Type)、target(Type)等,讀者可以自行嘗試以下,篇幅原因不做細(xì)談。
或者參考文章最后給出的 Demo 源碼,這個(gè) Demo 包含了本文演示的所有示例。
Demo 地址:AspectJDemo
AspectJ 參考手冊(cè):AspectJ.pdf
春節(jié)將至,希望已經(jīng)回家的、沒(méi)回家的和正在回家路上的你都能過(guò)一個(gè)開開心心的好年。
最后,預(yù)祝大家在新的一年里:身體健康,龍馬精神,事業(yè)有成,步步高升。