一、概念
AOP全稱呼 Aspect Oriented Programming ,國內(nèi)大致譯作面向切面編程,跟OOP(面向?qū)ο缶幊趟枷耄┮粯邮且环N編程思想,兩者間相互補(bǔ)充。通過預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。
說人話的講法可以大致這樣說:在一處地方編寫代碼,然后自動(dòng)編譯到你指定的方法中,而不需要自己一個(gè)方法一個(gè)方法去添加。這就是面向切面編程。
AOP既然是一種思想,那么就有多種對(duì)這種思想的實(shí)現(xiàn)。其實(shí)這個(gè)我并沒有做調(diào)研,推薦一下https://juejin.im/post/5c01533de51d451b80257752#heading-24
這篇文章中有對(duì)AOP的實(shí)現(xiàn)方案有一個(gè)全面的展示。
二、有什么用?(適用場景)
日志記錄,性能統(tǒng)計(jì),安全控制,事務(wù)處理,異常處理,熱修復(fù),權(quán)限控制等等等
將這些行為代碼從業(yè)務(wù)邏輯代碼中劃分出來,通過對(duì)這些行為的分離,我們希望可以將它們獨(dú)立到非指導(dǎo)業(yè)務(wù)邏輯的方法中,進(jìn)而改變這些行為的時(shí)候不影響業(yè)務(wù)邏輯的代碼。
最簡單日常開發(fā)需求,比如對(duì)點(diǎn)擊事件進(jìn)行埋點(diǎn)上傳行為數(shù)據(jù)、對(duì)方法進(jìn)行耗時(shí)的統(tǒng)計(jì)、防止點(diǎn)擊事件重復(fù)等。
假設(shè)要埋點(diǎn)的方法有幾百個(gè)那在每個(gè)方法都進(jìn)行同樣的編碼不僅顯得臃腫,并且當(dāng)需求變更的時(shí)候,涉及更改的地方有幾百個(gè)想想都覺得頭疼。這個(gè)是時(shí)候面向切面編程的作用就顯得非常重要了。
三、AOP的基本術(shù)語
- Joinpoint(連接點(diǎn)): 那些被攔截到的點(diǎn)(方法),可以是方法的前面、后面,或者異常、屬性等。
- Advice(通知\增強(qiáng)): 指攔截到 Joinpoint (方法)之后所要做的事情就是通知,也就是我們要寫的那些防止重復(fù)點(diǎn)擊事件什么的。
- Pointcut(切入點(diǎn)): 要對(duì)哪些Joinpoint (方法) 進(jìn)行攔截的定義。
- Introduction(引介):引介是一種特殊的通知在不修改類代碼的前提下, Introduction 可以在運(yùn)行期為類 動(dòng)態(tài)地添加一些方法或 Field。
- Target(目標(biāo)對(duì)象):代理的目標(biāo)對(duì)象。
- Weaving(織入):是指把增強(qiáng)應(yīng)用到目標(biāo)對(duì)象來創(chuàng)建新的代理對(duì)象的過程. AspectJ 采用編譯期織入和類裝在期織入 。
- Proxy(代理):一個(gè)類被 AOP 織入增強(qiáng)后,就產(chǎn)生一個(gè)結(jié)果代理類 。
- Aspect(切面):是切入點(diǎn)和通知(引介)的結(jié)合 。相當(dāng)于一個(gè)集合,這個(gè)集合包含所有的切點(diǎn)跟通知等
給一段AspectJ的代碼展示一下 加深印象:
@Aspect // 切面類 類下可以定義多個(gè)切入點(diǎn)和通知(引介)
public class TestAnnoAspectJava {
//自定義切點(diǎn)
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.threadTest())")
public void pointcut(){
}
//自定義切點(diǎn)
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.stepOn1(..))")
public void pointcutOn(){
}
//在切點(diǎn)pointcut()前面運(yùn)行
@Before("pointcut()")
public void before(JoinPoint point) {
}
//在切點(diǎn)pointcut()中運(yùn)行,圍繞的意思
//需要注意的是這個(gè)記得寫 joinPoint.proceed();
// 寫在代碼后面就是在切入原方法前面運(yùn)行
@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
}
//在切點(diǎn)pointcut()方法后面運(yùn)行
@After("pointcut()")
public void after(JoinPoint point) {
}
//在切點(diǎn)pointcut()方法返回后運(yùn)行
@AfterReturning("pointcut()")
public void afterReturning(JoinPoint point, Object returnValue) {
}
//在切點(diǎn)pointcut()拋異常后運(yùn)行
@AfterThrowing(value = "pointcut()", throwing = "ex")
public void afterThrowing(Throwable ex) {
}
}
-
注解圖解
注解.png - 切點(diǎn)表達(dá)式
<切入點(diǎn)指示符> (<@注解符>?<修飾符>? <返回類型> <方法名>(<參數(shù)>) <異常>?)
注意:注解符、 修飾符、異常 、參數(shù)(沒有參數(shù)的時(shí)候)可以省略,其它的不能省略
示例:
//正常方法等的切點(diǎn)
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.threadTest())")
public void pointcut(){ }
//注解的切點(diǎn)
@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
public void checkLogin() { }
- 通配符
*
:匹配任何字符;
…
:匹配多個(gè)任何字符,如在類型模式中匹配任何數(shù)量子包;在方法參數(shù)模式中匹配任何數(shù)量參數(shù)。
+
:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。
示例:
1. 匹配返回任何類型的修飾符,跟指定java文件下的`stepOn`開頭的方法名
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.stepOn*(..))")
public void pointcutOn() { }
2. 匹配com.mzs.aopstudydemo包下的所有String返回類型的方法
@Pointcut("execution(String com.mzs.aopstudydemo..*(..))")
public void afterReturning(JoinPoint point, Object returnValue) { }
3. 匹配所有public方法,在方法執(zhí)行之前打印"YOYO"。
@Before("execution(public * *(..))")
public void before(JoinPoint point) {
System.out.println("YOYO");
}
4. 匹配com.mzs包及其子包中的所有方法,當(dāng)方法拋出異常時(shí),打印"ex = 報(bào)錯(cuò)信息"。
@AfterThrowing(value = "execution(* com.mzs..*(..))", throwing = "ex")
public void afterThrowing(Throwable ex) {
System.out.println("ex = " + ex.getMessage());
}
- 切入點(diǎn)指示符
切入點(diǎn)指示符有好多,這里只用到了execution
其它的大家看一下https://blog.csdn.net/zhengchao1991/article/details/53391244這里就不展示了 有興趣的同學(xué)看一下這個(gè)文章
四、使用AspectJ(僅適用于Java,后面提供kotlin的處理方案)
-
基本概念
AspectJ
是一個(gè)實(shí)現(xiàn)AOP的思想的框架,完全兼容Java
,它有一個(gè)專門的編譯器用來生成遵守Java
字節(jié)編碼規(guī)范的Class
文件,只需要加上AspectJ
提供的注解跟一些簡單的語法就可以實(shí)現(xiàn)絕大部分功能上的需求了。
Android Studio與eclipse的導(dǎo)入方式不同,這里我展示的是Android studio的。(eclipse的話,麻煩同學(xué)百度下吧~~)
-
Gradle接入
- 在使用的
module
的build.gradle
下面添加
dependencies {
...
implementation 'org.aspectj:aspectjrt:1.8.9'
}
- 在使用的
module
的build.gradle
下面添加(跟android {}
同級(jí))
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
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.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;
}
}
}
}
-
開始使用
- 創(chuàng)建
TestAnnoAspectJava.java
類,并創(chuàng)建切點(diǎn)
/**
* Create by ldr
* on 2020/1/8 9:26.
*/
@Aspect
public class TestAnnoAspectJava {
@Pointcut("execution(* com.mzs.aopstudydemo.MainJavaActivity.test())")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint point) {
System.out.println("@Before");
}
@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("@Around");
joinPoint.proceed();
}
@After("pointcut()")
public void after(JoinPoint point) {
System.out.println("@After");
}
@AfterReturning("pointcut()")
public void afterReturning(JoinPoint point, Object returnValue) {
System.out.println("@AfterReturning");
}
@AfterThrowing(value = "pointcut()", throwing = "ex")
public void afterThrowing(Throwable ex) {
System.out.println("@afterThrowing");
System.out.println("ex = " + ex.getMessage());
}
}
- 在
com.mzs.aopstudydemo.MainJavaActivity
定義方法
public void test() {
System.out.println("Hello,I am LIN");
}
-------------------打印的信息
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @Before
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @Around
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: Hello,I am LIN
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @After
2020-01-09 15:38:53.903 18238-18238/com.mzs.aopstudydemo I/System.out: @AfterReturning
反編譯看一下生成的test
方法的源碼:
public void test() {
JoinPoint joinPoint = Factory.makeJP(ajc$tjp_0, this, this);
try {
TestAnnoAspectJava.aspectOf().before(joinPoint);
test_aroundBody1$advice(this, joinPoint, TestAnnoAspectJava.aspectOf(), (ProceedingJoinPoint)joinPoint);
} finally {
TestAnnoAspectJava.aspectOf().after(joinPoint);
}
}
在反編譯的源碼下可以看到,編譯后的源碼加上了TestAnnoAspectJava中定義的對(duì)應(yīng)邏輯。
還有一個(gè)關(guān)鍵點(diǎn)所有的通知都會(huì)至少攜帶一個(gè)JointPoint
參數(shù)
- Joinpoint(連接點(diǎn))提供給我們的一些方法
point.getKind() : method-execution //point的種類
point.getSignature() : void com.mzs.aopstudydemo.MainJavaActivity.stepOn1() // 函數(shù)的簽名信息
point.getSourceLocation() : MainJavaActivity.java:74 //源碼所在的位置
point.getStaticPart() : execution(void com.mzs.aopstudydemo.MainJavaActivity.stepOn1()) //返回一個(gè)對(duì)象,該對(duì)象封裝了靜態(tài)部分的連接點(diǎn)
point.getTarget() : com.mzs.aopstudydemo.MainJavaActivity@7992dfa //返回目標(biāo)對(duì)象
point.getThis() :com.mzs.aopstudydemo.MainJavaActivity@7992dfa //返回當(dāng)前對(duì)象
point.toShortString() : execution(MainJavaActivity.stepOn1())
point.toLongString() : execution(private void com.mzs.aopstudydemo.MainJavaActivity.stepOn1())
point.toString() : execution(void com.mzs.aopstudydemo.MainJavaActivity.stepOn1())
五 實(shí)踐:判斷是否登錄
-
前提:Java提供的元注解
image.png
關(guān)于怎么自定義注解之類不是本章的重點(diǎn),請(qǐng)大家可以看一下其它的相關(guān)類型的文章,下面切入正題~~
1. 自定義注解
創(chuàng)建注解類CheckLogin
,定義對(duì)應(yīng)的元注解信息,具體解釋看上面的圖。
并聲明一個(gè)isSkip
值。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckLogin {
boolean isSkip() default false;//增加額外的信息,決定要不要跳過檢查,默認(rèn)不跳過
}
2.定義切點(diǎn),定義通知
在切面類TestAnnoAspectJava
中
//定義一個(gè)變量模擬登錄狀態(tài)
public static Boolean isLoagin = false;
//定義切點(diǎn)
@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
public void checkLogin() {
}
//定義切入信息通知
@Around("checkLogin()")
public void checkLoginPoint(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//1. 獲取函數(shù)的簽名信息,獲取方法信息
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
//2. 檢查是否存在我們定義的CheckLogin注解
CheckLogin annotation = method.getAnnotation(CheckLogin.class);
//判斷是要跳過檢查
boolean isSkip = annotation.isSkip();
//3.根據(jù)注解情況進(jìn)行處理
if (annotation != null) {
if (isSkip) {
Log.i(TAG, "isSkip=true 這里不需要檢查登錄狀態(tài)~~~~~~");
proceedingJoinPoint.proceed();
} else {
if (isLoagin) {
Log.i(TAG, "您已經(jīng)登錄過了~~~~");
proceedingJoinPoint.proceed();
} else {
Log.i(TAG, "請(qǐng)先登錄~~~~~");
}
}
}
}
這里有
@Pointcut("execution(@com.mzs.aopstudydemo.CheckLogin * *(..))")
:切點(diǎn)表達(dá)式使用注解,一定是@+注解全路徑
!!
3. 使用
@CheckLogin()
public void LoginAfter(){
Log.i(TAG,"這里是登錄成功后才會(huì)顯示的數(shù)據(jù)——浪里個(gè)浪~~~");
}
@CheckLogin(isSkip = true)
public void unCheckLogin(){
Log.i(TAG,"這里是不需求要登錄判斷的~~~~");
}
button4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TestAnnoAspectJava.isLoagin = !TestAnnoAspectJava.isLoagin;
}
});
button5.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LoginAfter();
}
});
button6.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
unCheckLogin();
}
});
}
----------------------------------------------------------------------------------------
---------------點(diǎn)擊button6打印出來的Log-----------------------------------------
I/TestAnnoAspectJava: isSkip=true 這里不需要檢查登錄狀態(tài)~~~~~~
I/MainActivity: 這里是不需求要登錄判斷的~~~~
---------------先點(diǎn)擊button5,再點(diǎn)擊button4,再點(diǎn)擊button5---打印出來的Log------
I/TestAnnoAspectJava: 請(qǐng)先登錄~~~~~
I/TestAnnoAspectJava: 您已經(jīng)登錄過了~~~~
I/MainActivity: 這里是登錄成功后才會(huì)顯示的數(shù)據(jù)——浪里個(gè)浪~~~
六、兼容Kotlin
上面的示例用的是
Java
,但是如果使用Kotlin
的話就支持不了。所以需要的話可以使用滬江的gradle_plugin_android_aspectjx,簡稱AspectJX
這里就不做展示了。有需要的同學(xué)自己去翻看一下。
示例代碼地址
https://github.com/lovebluedan/AOPStudyDemo.git
感謝
https://github.com/feelschaotic/AndroidKnowledgeSystem/blob/master/7.%20%E8%BF%9B%E9%98%B6/AOP/AOP.md
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
https://juejin.im/post/5c01533de51d451b80257752#heading-24
http://www.lxweimin.com/p/aa1112dbebc7
https://blog.csdn.net/zhengchao1991/article/details/53391244