概念
AOP(Aspect Oriented Programming)簡而言之就是面向切面編程。它所要實現的目標就是解耦,提供代碼的靈活性和可擴展性。
與OOP的區別
OOP(Object Oriented Programming)面對對象編程。
這其實是兩種不同的設計思想:
- OOP是把同一對象的屬性放到一個對象里,把同一類別的類放到一個模塊里;這里同一的評判標準可以按照業務來劃分,也可以通過行為來劃分
- AOP則是提取相同的功能和方法,針對這樣的橫切面來歸類。
用圖來彌補下言語的匱乏:
AOP的應用領域
在Android開發過程中,我們時長遇到需要統計事件,性能監測,權限檢查等需求。而這些需求是獨立業務開發之外的。在業務開發過程中,開發人員不想被這些需求打擾而中斷了業務邏輯的梳理。這個時候,就可以用到AOP的思想來解決問題。
- 打印日志:獨立的日志模塊,在業務開發后期嵌入到各個業務模塊中,在代碼里不存在日志相關代碼。
- 性能監測:時長需要對生命周期函數,view繪制函數監測其運行時長來監測性能。但如果在代碼開發階段考慮就要,實現每個生命周期函數,在繪制view的函數中加入時間統計代碼,這樣不僅會導致冗余代碼還會影響方法本身的性能。
- 權限檢查:使方法功能單一,剝離權限檢查部分。
其實,總結起來,最終的目的就是為了解耦,盡量將業務無關的,且同一類方法,功能中需要做的重復動作,提取出來。
AspectJ
AOP的實現有很多中,AspectJ只是其中一種,在Java中用得比較多。AspectJ可以說是一種語言,它完全兼容Java,使用原生的Java來開發的話,只需要加上AspectJ的注解就可以。因此兩種方式:
- 通過AspectJ的關鍵字來實現
- 原生java+AspectJ的相關注解來開發
但是無論是通過何種方式實現,其編譯都必須要通過AspectJ的編譯工具ajc來編譯。
AspectJ的語法
在介紹AspectJ的語法之前,先介紹幾個概念,也是AspectJ中的關鍵字,了解他們的含義,對于開發至關重要。
- aspect(切面) 針對切面的模塊。也就說獨立于業務,需要被插入的部分。
- joinpoint(連接點) 顧名思義就是連接切面模塊和業務模塊的地方;也可以理解為就是業務模塊中需要被嵌入代碼的地方。
- pointcut 這個理解起來跟joinpoint應該是一個意思,只不過它可以添加一些附加條件
- advice(處理邏輯) 說邏輯處理有點牽強,它表示的意思應該是被插入的代碼,以及插入的時機,如:Before,After,Around等。
常用的切入點
切入點一般是通過joinpoint和advice的組合來實現的,常用的可以看下表:
joinpoint | advice | 切入點 |
---|---|---|
execution | before | 方法執行之前,切入點在方法內 |
execution | after | 方法執行之后,切入點在方法內 |
execution | around | 方法執行前后,可以替換原方法,切入點在方法內 |
call | before | 方法調用之前,切入點在方法外 |
call | after | 方法調用之后,切入點在方法外 |
call | around | 方法調用前后,可以替換原方法,切入點在方法外 |
PS:以上是常用的一些切入點,還有通過cflow來切入每一行字節碼。這個控制較難,控制不好會產生StackOverFlow,這個以后再說。
PSS: Advice的各個類型是可以組合使用的,但是切記Around與After是不可以同時使用的,會發生重復調用的問題。
JoinPoint的匹配規則
通過call 和 execution 我們可以知道切入點的時機是在方法調用還是在方法執行。但是如何才能找到方法呢,這就需要一定的匹配規則去找到需要切入的方法。
舉個例子:
cn.test.fwl.Test.main() 這樣的表達式,可以指定到包名為<cn.test.fwl>,類名為<Test>中無參數的main方法;那么如果我們需要匹配到這個類里所有的main方法,又或者我們需要匹配到這個包里所有類的main方法,再或者我們需要匹配到包含main字符的方法該如何來寫表達式呢?
通配符
AspectJ中提供了一些通配符來方便我們找到滿足規則的方法。
通配符 | 含義 |
---|---|
* | 匹配除了[.]之外的所有字符,用在路徑中表示任意包名字符串,用在類名中標識任意類名字符串,方法中表示任意方法名字符串 |
.. | 表示任意的子package,或者任意的參數 |
+ | 表示子類 |
舉例:
- java.*.Date : 可以表示java.sql.Date,也可以表示java.util.Date
- Test* : 表示以Test開頭的任意字符串
- java..* : 表示java包中的任意類
- java..*Model+ : 表示java包中以Model結尾的類的子類
- test(..) : 表示方法名為test,任意的參數,沒有參數,有一個,兩個都可以匹配
- test(int,char) : 表示方法名為test,有且僅有兩個參數,類型為int,char
- test(String,..) : 表示方法名為test,至少有一個參數,第一個類型為String,其他任意
- test(String ...) : 表示方法名為test,參數個數不定,但必須都是String類型,這里的[...] 不是通配符,而是java中不定參數的意思。
JoinPoint的約束
除了上面的匹配規則外,AspectJ還提供了一些其他方法來更加精確的選擇JoinPoint,比如某個類中的JoinPoint或者某個函數執行流程中的JoinPoint。
關鍵詞 | 說明 | 舉個栗子 |
---|---|---|
within(pattern) | pattern可以通過通配符表示,代表某個包或者類 | 滿足pattern適配的JoinpPoint。比如說within(Test)就標識在Test類中(包括內部類)所有的JoinPoint。 |
withinCode(Constructor Signature/Method Signature) | 表示某個構造函數或其他函數執行過程中涉及到的 JoinPoint | 比如:withinCode(* Test.testMethod(..))表示testMethod涉及的JoinPoint; withinCode(*.Test.new(..))表示Test構造函數涉及的JoinPoint |
cflow(pointcuts) | cflow的條件是一個pointcut,表示某個流程中涉及的JoinPoint | 比如cflow(call Test.testMethod):表示調用Test.testMethod函數時所包含的JoinPoint,包括testMethod的call這個JoinPoint本身 |
cflowbelow(pointcuts) | 比如:cflowbelow(call Test.testMethod):表示調用Test.testMethod函數時所包含的JoinPont,不包含testMethod的call這個JoinPont本身 | |
this(Type) | JoinPoint的this對象是Type類型。包括其子類 | JPoint所在的這個類的類型是Type標示的類型或是其子類,則和它相關的JPoint將全部被選中。比如:Animal中的Move方法,則Bird,cat中的Move方法都會被選中 |
target(Type) | JoinPoint的target對象是Type類型 | target一般用在call的情況。call一個函數,這個函數可能定義在其他類。比如Bird的move方法在調用時被選中,那么其他的Move的方法則不會。 |
args(Type) | 用來對JoinPoint的參數進行條件約束 | 比如args(int,..),表示第一個參數是int,后面參數個數和類型不限 |
Advice的注意點
關于Advice前面已經說過了,他其實就是被嵌入的部分,而嵌入的時機,也在切入點的表格里提到過。這里主要講下注意點:
- After:表示函數執行或者調用完成后運行被嵌入的代碼部分。但是函數可能執行結束可能有兩種退出方式:一個正常的Return,或者拋出異常,因此After也做了區分: after():return(type) 和
after():throwing(Throwable) - Around: 除了之前說Around和After不能同時使用之外,Around因為是可以替代原函數執行的,因此,要特別注意被嵌入的代碼的返回值一定要和原來的方法一致。
環境配置
Eclipse
Android現在很少有用Eclipse開發的了,但是Eclipse的插件卻是對AspectJ開發支持最友好的。基礎的AOP實例,打算用Eclipse來開發AspectJ對Java的橫切,因此,這里也介紹下,Eclipse的搭建。
Help -> Install New Software
然后一直下一步就好
Android Studio
首先在工程目錄中導入相關的編譯工具:
buuildscript{
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
然后在Aspect的module的build.gradle中添加依賴庫:
compile 'org.aspectj:aspectjrt:1.8.9'
添加aspect編譯的腳本:
def variants = android.libraryVariants
variants.all{ variant ->
LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
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
)
]
MessageHandler handler = new MessageHandler(true);
new Main().run(args,handler)
def log = project.logger
for(IMessage msg: handler.getMessages(null,true)){
switch(msg.getKind()){
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error msg.message, msg.thrown
break;
case IMessage.WARNING:
log.warn msg.message, msg.thrown
break;
case IMessage.INFO:
log.info msg.message,msg.thrown
break;
case IMessage.DEBUG:
log.debug msg.message,msg.thrown
break;
}
}
}
}
最后在app的module里添加對aspect module的依賴,同時添加上對aspectJ編譯的腳本:
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 msg: handler.getMessages(null,true)){
switch (msg.getKind()){
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error msg.message,msg.thrown
break;
case IMessage.WARNING:
log.warn msg.message, msg.thrown
break;
case IMessage.INFO:
log.info msg.message,msg.thrown
break;
case IMessage.DEBUG:
log.debug msg.message,msg.thrown
break;
}
}
}
}
舉個栗子
先通過Eclipse上創建AJ的工程來熟悉下AspectJ的相關語法。
首先,創建一個AspectJ的工程,在已經完成AJDT的插件的前提下,在新建工程的時候,就可以看到可以創建AspectJ Project
這樣的工程,如圖:
之后創建兩個不同的包,來區分java文件和aj文件
創建Test1.java文件
public class Test1 {
public static void main(String[] args) {
test();
}
public static void test(){
System.out.println("this is test method!");
}
}
現在我們要在test()
方法執行打印之前,插入我們的操作(這里也插入一句打印)
注意我們這里創建文件的時候,不再是java文件,而是.aj的文件
創建AspectJ.aj文件
public aspect AspectJ{
public pointcut aspect1(): execution(* test(..));
before():aspect1(){
System.out.println("this is before test method: execution");
}
}
完成這個文件之后,就會發現之前Test1.java
中test()
這個方法里上多了箭頭的標志。這就表明插入成功了。
可以運行看下結果:
AspectJ.aj中的注入的打印已經被打印出來了。那么被注入之后的
Test1.class
是樣的:
可以看到在打印System.out.println("this is test method!");
之前被插入了一段代碼,而這段正是before():aspect1()
方法中所執行的內容。
上面以execution
和before
的組合舉了一個簡單的例子,主要是闡述了下如何創建Aspecj的工程,以及相應的文件。下面的例子會包含call
,execution
和before
,after
的兩兩組合。
Test.java
public class Test1 {
public static void main(String[] args) {
testBeforeExecution();
testBeforeCall();
testAfterExecution();
testAfterCall();
testAfterReturn();
testAfterThrowable();
}
public static void testBeforeExecution(){
System.out.println("this is test before-execution!");
}
public static void testBeforeCall(){
System.out.println("this is test before-call");
}
public static void testAfterExecution(){
System.out.println("this is test after-execution");
}
public static void testAfterCall(){
System.out.println("this is test after-call");
}
public static String testAfterReturn(){
String a = "test parameter";
System.out.println("this is test after-return");
return a;
}
public static String testAfterThrowable(){
String a = null;
System.out.println("this is test after-throwable");
a.equals("test");
return a;
}
}
AspectJ.aj
public aspect AspectJ{
public pointcut aspect1(): execution(* testBeforeExecution(..));
public pointcut aspect2(): call(* testBeforeCall(..));
public pointcut aspect3(): execution(* testAfterExecution(..));
public pointcut aspect4(): call(* testAfterCall(..));
public pointcut aspect5(): execution(* testAfterReturn(..));
public pointcut aspect6(): execution(* testAfterThrowable(..));
before():aspect1(){
System.out.println("this is before test : execution");
}
before():aspect2(){
System.out.println("this is before test: call");
}
after():aspect3(){
System.out.println("this is after test:execution");
}
after():aspect4(){
System.out.println("this is after test: call");
}
after() returning(String s):aspect5(){
System.out.println("this is after test : return->"+s);
}
after() throwing(Exception e):aspect6(){
System.out.println("this is after test: throwable->"+e.getMessage());
}
}
可以看到運行結果:
同時也可以看到編譯后的class文件:
接下來再舉個關于Around的用法的例子:
Test2.java
public class Test2 {
public static void main(String[] args) {
testAroundCall();
testAroundExecution();
testAroundReplace();
System.out.println(testAroundRetrun());
}
public static void testAroundCall(){
System.out.println("this is testAroundCall method");
}
public static void testAroundExecution(){
System.out.println("this is testAroundExecution method");
}
public static void testAroundReplace(){
System.out.println("this is testAroundReplace");
}
public static String testAroundRetrun(){
String a = "the return value";
System.out.println("this is test around return");
return a;
}
}
AspectJ1.aj
public aspect AspectJ1{
public pointcut test1():execution(* testAroundCall(..));
public pointcut test2():call(* testAroundExecution(..));
public pointcut test3():call(* testAroundReplace(..));
public pointcut test4():call(* testAroundRetrun(..));
void around():test1(){
System.out.println("around-execution test before");
proceed();
System.out.println("around-execution test after");
}
void around():test2(){
System.out.println("around-call test before");
proceed();
System.out.println("around-call test after");
}
void around():test3(){
System.out.println("do replace ... ");
}
String around():test4(){
String a = "string in aspect";
System.out.println("replace return value");
proceed();
return a;
}
}
運行結果如下:
class 文件
以上都是AspectJ語言寫的,那么如果使用純Java的方式該如何來實現呢,看下面的例子:
Aspectj2.java
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class Aspectj2 {
@Pointcut("execution(* testBeforeExecution(..))")
public void test1(){
}
@Pointcut("call(* testBeforeCall(..))")
public void test2(){
}
@Pointcut("execution(* testAfterExecution(..))")
public void test3(){
}
@Pointcut("call(* testAfterCall(..))")
public void test4(){
}
@Pointcut("execution(* testAfterReturn(..))")
public void test5(){
}
@Pointcut("execution(* testAfterThrowable(..))")
public void test6(){
}
@Before("test1()")
public void execute1(){
System.out.println("before-execution aspectj");
}
@Before("test2()")
public void execute2(){
System.out.println("before-call aspectj");
}
@After("test3()")
public void execute3(){
System.out.println("after-execution aspectj");
}
@After("test4()")
public void execute4(){
System.out.println("after-call aspectj");
}
@AfterReturning("test5()")
public void execute5(){
System.out.println("after-return aspectj");
}
@AfterThrowing("test6()")
public void execute6(){
System.out.println("after-throw aspectj");
}
}
特別提醒下:類的注釋@Aspect千萬不能少,在這入坑了好幾次
運行結果如下:
再看下編譯后的文件:
栗子就先吃這么多~~~后面會再補一篇關于帶參數,返回值處理的栗子。
AspectJ在Android中的應用
后續會在github上傳一個關于權限檢查的庫,有時間也會寫個文檔介紹下這個庫。
總結
AOP的知識接觸得還不多,寫了些demo和Android的庫,總結下來,重點還是在JoinPoint的適配,如何才能精確得適配到自己想要的切入點,還需要將JoinPoint和Advice結合多加練習。
Eclipse上對的AJDT的插件對Aspect的語法還有錯誤檢查,但是Android Studio上還沒有,所以寫的時候,要特別仔細。
TODO
- 帶參數,返回值的栗子
- 權限檢查的工程和分析文檔