Android Aspectj 從入門到實戰
文章目錄
AOP 簡介
Android Studio 想接入 AspectJ ? 看這篇就對了!從0到1 , 包會!
OOP ( Object Oriented Programming ) 面向物件程式設計思想
AOP ( Aspect Oriented Programming ) 面向切面程式設計思想
- OOP : 面向物件即針對業務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元模組劃分。也就是把各個獨立的功能封裝為個體或模組。
如下圖,每一個模組封裝了其特有的功能屬性,各盡其責,便於其它使用者的呼叫和複用。
- AOP : 針對業務處理過程中特定的切面
如下圖,在特定的切面進行程式碼 「織入」(注意用詞,是織入不是hook…),新增共同邏輯( 日誌,埋點等) 且不會影響原有模組的業務功能和架構。
OOP 的精髓是把功能或問題模組化,每個模組處理自己的家務事。但在現實世界中,並不是所有問題都能完美得劃分到模組中。舉個最簡單而又常見的例子:現在想為每個模組加上日誌功能,要求模組執行時候能輸出日誌並統計當前方法耗時。
這個問題放在 OOP 思想中解決的辦法通常是設計日誌模組,並且在需要統計的地方一一手動加入日誌的 API,並且如果日誌的 API 改動,那將牽一髮而動全身。
這個時候 AOP 的用途便體現出來了。
AOP 主要用途有:日誌記錄,行為統計,安全控制,事務處理,異常處理,系統統一的認證、許可權管理等。可以使用 AOP 思想將這些程式碼從業務邏輯程式碼中劃分出來,通過對這些行為的分離,可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行為的時候不影響業務邏輯的程式碼。
Android AOP 實現原理
上圖中 AspectJ 是 Android 實現 AOP 程式設計思想的具體工具
AspectJ 的使用核心就是它的編譯器,它就做了一件事,將 AspectJ 的程式碼在編譯期插入目標程式當中,執行時跟在其它地方沒什麼兩樣,因此要使用它最關鍵的就是使用它的編譯器去編譯程式碼 (AspectJ compile) 。
ajc 會構建目標程式與 AspectJ 程式碼的聯絡,在編譯期將 AspectJ 程式碼插入被切出的 PointCut 中,達到 AOP 的目的。
也就是在 .java
檔案編譯為.class
檔案的時候,將.java
檔案做手腳,對相應切入點的程式碼進行功能程式碼「織入」。
Android AOP 基本實現方式
上面說道 : AOP是一個程式設計思想和概念,本身並沒有設定具體語言的實現,這實際上提供了非常廣闊的發展的空間。
AspectJ 是 AOP 的一個很悠久的實現,它能夠和 Java 配合起來使用。( 很穩 , 支付寶app第三方開源也有用到)
先來了解幾個 AspectJ 的 基本和主要的 關鍵詞 :
- Aspect : Aspect 宣告類似於 Java 中的 類宣告 ,在Aspect中會包含著一些 Pointcut (切入點)以及相應的 Advice (通知) , 「Pointcut 和 Advice 的組合可以看做切面」
- Advice : ( 通知 ) , 定義了在 Pointcut 裡面定義的程式點具體要做的操作,它通過 before、after 和 around 來區別是在每個JoinPoint 之前、之後還是完全替代目標方法 的程式碼。 --> when
- Pointcut : ( 切入點 ) , 告訴程式碼注入工具,在何處注入一段特定程式碼的表示式。 --> where
下面是 Pointcut 篩選和匹配條件.(為了更精確的找到要切入地方.)
篩選條件 | 說明 | 示例 |
---|---|---|
within(TypePattern) | 篩選執行的包名路徑 | within(com.sample.aop.*),在aop包名內的JPoint. |
withincode(Method) | 篩選執行的方法. | withinCode(* A.aopMethod(…)),在A類的aopMethod涉及的JPoint |
target(類全限定名) | target一般用在call的情況,匹配任意標註了的目標類(指明攔截的方法屬於那個類) | target(A)就會搜尋到由A類呼叫testMethod的地方 |
this(類全限定名) | 與target雷同,區分點在於:this指方法是在哪個類中被呼叫的 | B類中呼叫A.testMethod,指定的類為B |
args() | 對入參進行條件匹配 | args(int,…),表示第一個引數是int,後面引數個數和型別不限 |
… | 其它高階用法 |
- JoinPoint : ( 連線點) , 表示在程式中明確定義的點,例如,典型的 方法函式呼叫 , 類成員的訪問 以及對 異常處理程式塊 的執行等等,這些都是 JPoint 。(如
Log.e()
這個函式 ,e()
可以看作是個 JPoint ,而且呼叫e()
的函式也可以認為是一個 JPoint ) , 也就是所有可以注入程式碼的地方。
可以說如果 AspectJ 規定中沒有這樣的 JPoint,那麼我們是無法利用AspectJ 來實現功能需求.
織入時機 | 說明 | 示例 |
---|---|---|
call | 函式呼叫 | 比如呼叫Log.e() , 這是一處JPoint |
execution | 函式調執行 | execution是某個函式執行的內部 |
例如 A 類中,呼叫 Pointcut.Method() ,
call 擷取的是 在A類中呼叫該處函式的地方.
execution 擷取的則是 Pointcut 內 Method() 執行的方法…
Call(Before)
Pointcut{
Pointcut Method
}
Call(After)
Pointcut{
execution(Before)
Pointcut Method
execution(After)
}
Demo
實體類 , get/setName方法
下面這個例子通過 AOP 修改getName()
返回引數,在呼叫setName()
方法加上列印日誌
public class AopDemo {
public static class innerB {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
宣告使用 Aspect 的類, 加上@Aspect 註解即可,類裡定義了切入點和通知,即組成了切面.
@Pointcut(" call(* getName() ) ")
- call 表示方法函式呼叫即要擷取的地方
- 第一個位置表示方法呼叫的返回值,*表示返回值為任意型別
- getName表示方法名,也可以使用該方法的全限定名,getName()的括號()代表這個方法的引數,可以指定型別,或者(…)(int,…)這樣來代表任意型別和個數的引數。
- 同時在call()後面可以 加入&&、||、!來進行條件組合,匹配或過濾關鍵 JPoint
@Aspect
public class DemoAspect {
@Pointcut("call(* setName(String))")
public void demo2() {}
@Around("demo2()")
public Object arounddemo1(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
Object proceed = joinPoint.proceed();
if (target instanceof AopDemo.innerB) {
Log.e("log", "call setName");
// ((AopDemo.innerB) target).setName("haha"); // 可以在方法執行之後搞點事情.
}
// joinPoint.proceed()代表執行原始的方法,在這之前之後都可以進行各種邏輯處理。
return proceed;
}
@Pointcut("call(* getName())")
public void demo1() {}
@Around("demo1()")
public String arounddemo1() {
return "hoho";
}
}
方法調用出 , 例如在MainActivity裡呼叫實體類的get/setName方法
// MainActivity裡呼叫方法
AopDemo.innerB innerB = new AopDemo.innerB();
innerB.setName("ok");
Toast.makeText(this, "str==" + innerB.getName(), Toast.LENGTH_SHORT).show();
innerB.setName(“ok”); 程式碼執行後便會加上 Log.e(“log”, “call setName”);
innerB.getName(); 返回的不是"ok",而是我們程式碼織入的"hoho"
開啟當前MainActivity的.class檔案,看看 AspectJ 到底做了什麼騷操作
// MainActivity.class AspectJ 程式碼織入前
innerB innerB = new innerB();
innerB.setName("ok");
Toast.makeText(this, "str==" + innerB.getName(), 0).show();
// MainActivity.class AspectJ 程式碼織入後
innerB innerB = new innerB();
String var7 = "ok";
JoinPoint var9 = Factory.makeJP(ajc$tjp_0, this, innerB, var7);
var10000 = DemoAspect.aspectOf();
Object[] var10 = new Object[]{this, innerB, var7, var9};
var10000.arounddemo1((new MainActivity$AjcClosure1(var10)).linkClosureAndJoinPoint(4112));
Toast.makeText(this, "str==" + DemoAspect.aspectOf().arounddemo1(), 0).show();
如果將 Pointcut 的 call 方法改為 execution , 修改的則是 innerB.class 檔案.
也就是說 AspectJ 實現 AOP 程式設計思想的方法就是在 .java 檔案編譯為 .class 期間,使用 ajc (AspectJ compile) 編譯器 , 將需要 織入 的程式碼插到特定的 Pointcut 中
以上便是 AspectJ 基本使用方式,要挑戰高階用法,前往 AspectJ 開發手冊.
自定義PointCut
例如我們要在程式碼中進行許可權檢查,如果沒有許可權則不執行方法(或者執行許可權呼叫方法,許可權申請成功後再執行目標方法)
- 1.建立自定義註解行為
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomPointCut {
String[] permissionName();
}
- 2.方法呼叫,在MainActivity呼叫註解方法
@CustomPointCut(permissionName = {"PHONE", "STATUS"})
public void customMethod() {
Toast.makeText(this, "customMethod call", Toast.LENGTH_SHORT).show();
}
- 3.宣告使用 Aspect 的類
@Aspect
public class DemoAspect {
// 具體使用的時候,CustomPointCut 要改為具體的全限定名.
@Pointcut("execution(@com.xxx.CustomPointCut * *..*.*(..))")
public void customMethod() {
}
@Around("customMethod()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 類名
String className = methodSignature.getDeclaringType().getSimpleName();
// 方法名
String methodName = methodSignature.getName();
// 功能名
CustomPointCut behaviorTrace = methodSignature.getMethod().getAnnotation(CustomPointCut.class);
String[] value = behaviorTrace.permissionName();
// value -- > phone,status
long start = System.currentTimeMillis();
// 也可以不執行joinPoint.proceed(),根據業務需求沒有許可權/登入不呼叫目標方法
Object result = joinPoint.proceed();// result 為目標方法呼叫後的返回值
long duration = System.currentTimeMillis() - start;//可以統計方法耗時.
return result;//返回值,可以任性修改.
}
}
這樣只要程式碼中加入 @CustomPointCut 註解,便可以統一處理許可權操作. 登入判斷也可以用此方法來統一處理。程式碼精簡,一步到位.
下面看一下 AspectJ 在編譯時期做了哪些處理
// MainActivity.class
@CustomPointCut(
permissionName = {"PHONE", "STATUS"}
)
public void customMethod() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
DemoAspect var10000 = DemoAspect.aspectOf();
Object[] var2 = new Object[]{this, var1};
var10000.aroundMethod((new MainActivity$AjcClosure1(var2)).linkClosureAndJoinPoint(69648));
}
Android AspectJ 接入實戰
1.新增aop模組,配置依賴
建議新建一個aop相關模組 , 方便 aop 作為Demo 專案的除錯,後期可以輕鬆依賴進自己的工程專案。
首先建立一個 lib-aop 模組,作為 library 方式引入到 Demo 專案中
關鍵有以下兩點 :
- 依賴 ajc 編譯指令碼,我們將指令碼內容寫 aspectj-configure-lib.gradle 檔案中,aop 模組引用該指令碼
- 新增 aspectjrt 依賴,新增的方式有兩種,如果是多人協作建議下載 jar 包配置到本地依賴.
aspectj 相關jar包 : maven倉庫地址.
示例專案中所用 jar 包 : aspectjrt-1.8.13.jar. ( 放置 lib-aop 模組內 libs 資料夾下)
// aop 模組內 build.gradle
apply plugin: 'com.android.library'
apply from: '../lib-aop/aspectj-configure-lib.gradle' // ajc 編譯所需gradle指令碼
android {
compileSdkVersion 27
defaultConfig {
minSdkVersion 17
targetSdkVersion 27
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:support-v4:27.1.1'
//依賴方式 1. 使用本地jar包
api fileTree(include: ['*.jar'], dir: 'libs') // 作用範圍一定得是 api !!!
//依賴方式 2. 配置 maven 地址
// api 'org.aspectj:aspectjrt:1.8.13'
}
- 踩坑1 :aspectjrt 依賴的作用範圍一定得是 api ,否則其它模組死活織入不了程式碼
2.配置 ajc 指令碼
建議 在 lib-aop 模組 內新建 aspectj-configure-lib.gradle 檔案,指令碼內容為以下程式碼
// aspectj-configure-lib.gradle
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.13'
classpath 'org.aspectj:aspectjweaver:1.8.13'
}
}
repositories {
mavenCentral()
}
android.libraryVariants.all { variant ->
if (variant.buildType.isDebuggable()) {
// return; //開放後debug模式不生效
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", android.bootClasspath.join(
File.pathSeparator)]
MessageHandler handler = new MessageHandler(true)
new Main().run(args, handler)
def log = project.logger
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:
case IMessage.INFO:
log.info message.message, message.thrown
break
case IMessage.DEBUG:
log.debug message.message, message.thrown
break
}
}
}
}
- 踩坑2 :aspectj-configure-lib.gradle 只適用於lib 模組!
在 aspectj-configure-lib.gradle 的第 21行中配置到 :android.libraryVariants.all
,這隻適用於 型別為 library 的 module 享用
application 的 module 只需要將這行配置改為android.applicationVariants.all
,即可.
3.配置 app 的指令碼
在 Demo 工程中的 app 目錄下,配置 build.gradle 指令碼檔案
// build.gradle
apply plugin: 'com.android.application'
// ajc 編譯所需gradle指令碼,application適用
apply from: '../lib-aop/aspectj-configure-app.gradle'
android {
compileSdkVersion 27
defaultConfig {
applicationId "com.demo.aop"
minSdkVersion 17
targetSdkVersion 27
versionCode 1
versionName "1.0"
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation project(":lib-aop")// 依賴 aop 模組,這裡對作用範圍沒有限制.
}
上文說到 aspectj-configure-app.gradle 與 aspectj-configure-lib.gradle 的不同點只在於 第22行,
// aspectj-configure-app.gradle
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.13'
classpath 'org.aspectj:aspectjweaver:1.8.13'
}
}
repositories {
mavenCentral()
}
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (variant.buildType.isDebuggable()) {
// return; //開放後debug模式不生效
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-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
}
}
}
}
4.細節提醒
如此一來,aspectj 便配置完成了,另外,下面幾點細節提醒 :
- lib-aop 模組內需配置 proguard-rules.pro 混淆相關,將 lib-aop 內相關程式碼的包名新增進配置中,防止混淆.
-dontwarn com.xxx.aop.**
-keep class com.xxx.aop.**{*;}
- 其它模組如果需要在編譯時期使用 aspectj 程式碼織入功能,需要加入gradle指令碼 ajc 配置
app : apply from : '../lib-aop/aspectj-configure-app.gradle'
library : apply from : '../lib-aop/aspectj-configure-lib.gradle'
並且依賴 lib-aop 模組(如果有公用base模組,作用範圍可以用 api ,其它模組就可以不用再次新增依賴了。)
implementation project(":lib-aop")// 依賴 aop 模組
- 踩坑3 :lib-aop 不能打成 aar !
編寫 aspect 相關的模組不能打成 aar ,需要模組專案引用,否則編譯打包後會找不到類java.lang.NoClassDefFoundError: Failed resolution of: xxx/xxx/具體類
- 踩坑4 :J神的 hugo 外掛 debug 時期 ajc 編譯無效 , 因為在 ajc 編譯指令碼 gradle 中,如果是 Debuggable , return … ( 這一點估計坑了許多人. )
if (variant.buildType.isDebuggable()) {
return;
}
- 踩坑5 :AS編譯時期 IOException ,或檔案被佔用.
java.lang.RuntimeException: java.io.IOException: Failed to delete C:\Users\..\build\intermediates\intermediate-jars\debug\classes.jar
關閉工作管理員中 java.exe 程序 ,再次編譯即可.
參考
先理清概念 : Android AOP面向切面程式設計AspectJ.
再深入瞭解 阿拉神農的 :深入理解Android之AOP. 大多數部落格都參考該篇文章
最後android配置 aspectj : AndroidStudio 配置 AspectJ 環境實現AOP.