前言:
在我們程序開(kāi)發(fā)中,嘗嘗會(huì)遇到同一事件多個(gè)地方調(diào)用的情況,比如說(shuō)用戶行為統(tǒng)計(jì),登錄狀態(tài)檢查,返回鍵事件處理,如果這些情況使用常規(guī)的OOP方式去實(shí)現(xiàn),勢(shì)必會(huì)造成大量重復(fù)代碼,而且耦合性也比較高,所以我們是否有其他更好的方式去實(shí)現(xiàn)呢,這就是我們今天要了解的AOP實(shí)現(xiàn)方案,它可以將多個(gè)同一事件統(tǒng)一處理,實(shí)現(xiàn)切面效果。
Github下載地址:
https://github.com/VincentStory/AopProject
1.首先要了解什么是AOP
AOP是Aspect Oriented Programming的縮寫,即“面向切面編程”,就是針對(duì)同一類問(wèn)題的統(tǒng)一處理,通過(guò)預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。AOP是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開(kāi)發(fā)的效率。
2.了解一下AspectJ
AspectJ是對(duì)AOP編程思想的一個(gè)實(shí)踐方案,AOP是一種思想,它在各個(gè)語(yǔ)言中都有各自的實(shí)踐方案,那AspectJ是Java中比較火的實(shí)踐方案,它可以完全兼容Java。當(dāng)然,除了使用AspectJ特殊的語(yǔ)言外,AspectJ還支持原生的Java,只要加上對(duì)應(yīng)的AspectJ注解就好。所以,使用AspectJ有兩種方法:
(1)完全使用AspectJ的語(yǔ)言,和Java幾乎一樣,也能在AspectJ中調(diào)用Java的任何類庫(kù)。AspectJ只是多了一些關(guān)鍵詞。
(2)使用純Java語(yǔ)言開(kāi)發(fā),然后使用AspectJ注解,簡(jiǎn)稱@AspectJ。
基礎(chǔ)概念:
Aspect 切面:切面是切入點(diǎn)和通知的集合。
PointCut 切入點(diǎn):切入點(diǎn)是指那些通過(guò)使用一些特定的表達(dá)式過(guò)濾出來(lái)的想要切入Advice的連接點(diǎn)。
Advice 通知:通知是向切點(diǎn)中注入的代碼實(shí)現(xiàn)方法。
Joint Point 連接點(diǎn):所有的目標(biāo)方法都是連接點(diǎn).
Weaving 編織:主要是在編譯期使用AJC將切面的代碼注入到目標(biāo)中, 并生成出代碼混合過(guò)的.class的過(guò)程
3.具體實(shí)踐
(1)首先在project中的build.gradle中配置AspectJ,如下:
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.2"
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
(2)在app的build.gradle中配置AspectJ編譯器
buildscript { // 編譯時(shí)用Aspect專門的編譯器,不再使用傳統(tǒng)的javac
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
plugins {
id 'com.android.application'
}
android {
compileSdk 31
defaultConfig {
applicationId "com.vincent.aop.project"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
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;
}
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'org.aspectj:aspectjrt:1.8.13'
}
(3)創(chuàng)建切面AspectJ,具體處理切面統(tǒng)一事件
@Aspect // 定義切面類
public class LoginCheckAspect {
private final static String TAG = "aop_tag >>> ";
// 1、應(yīng)用中用到了哪些注解,放到當(dāng)前的切入點(diǎn)進(jìn)行處理(找到需要處理的切入點(diǎn))
// execution,以方法執(zhí)行時(shí)作為切點(diǎn),觸發(fā)Aspect類
// * *(..)) 可以處理ClickBehavior這個(gè)類所有的方法
@Pointcut("execution(@com.vincent.aop.project.annotation.LoginCheck * *(..))")
public void pointCut() {
}
// 2、對(duì)切入點(diǎn)如何處理
@Around("pointCut()")
public Object joinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
//1.獲取切入點(diǎn)所在目標(biāo)對(duì)象
Object targetObj =joinPoint.getTarget();
Log.e(TAG, "類名:"+targetObj.getClass().getName());
// 2.獲取切入點(diǎn)方法的名字
String methodName = joinPoint.getSignature().getName();
Log.e(TAG, "切入方法名字:"+methodName);
// 3. 獲取方法上的注解
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
LoginCheck apiLog= method.getAnnotation(LoginCheck.class);
Log.e(TAG, "切入方法注解的title:"+apiLog.title());
}
//4. 獲取方法的參數(shù)
Object[] args = joinPoint.getArgs();
for(Object o :args){
Log.e(TAG, "切入方法的參數(shù):"+o);
}
Context context = (Context) joinPoint.getThis();
if (MyApplication.isLogin()) {
Log.e(TAG, "檢測(cè)到已登錄!跳轉(zhuǎn)其他頁(yè)面");
return joinPoint.proceed();
} else {
Log.e(TAG, "檢測(cè)到未登錄!");
Toast.makeText(context, "請(qǐng)先登錄!", Toast.LENGTH_SHORT).show();
context.startActivity(new Intent(context, LoginActivity.class));
return null; // 不再執(zhí)行方法(切入點(diǎn))
}
}
}
上面除了登錄檢查的邏輯還添加了一些獲取類名,方法名,方法參數(shù)值,注解參數(shù)值的方法,根據(jù)項(xiàng)目需要來(lái)具體選擇是否獲取。
(3)在Activity頁(yè)面使用AspectJ
@LoginCheck
public void myInfomation(View view) {
Log.e(TAG, "開(kāi)始跳轉(zhuǎn)到 -> 我的資料 Activity");
}
@LoginCheck
public void myMoney(View view) {
Log.e(TAG, "開(kāi)始跳轉(zhuǎn)到 -> 我的余額 Activity");
}
public void myScore(View view) {
checkLogin("參數(shù)1", "參數(shù)2");
}
@LoginCheck(title = "登錄檢查", isSaveRequestData = false)
public void checkLogin(String str, String str2) {
Log.e(TAG, "開(kāi)始跳轉(zhuǎn)到 -> 我的積分 Activity");
}
點(diǎn)擊跳轉(zhuǎn)之后打印信息如下:
2021-11-15 10:53:43.930 6665-6665/com.vincent.aop.project E/aop_tag >>>: 類名:com.vincent.aop.project.MainActivity
2021-11-15 10:53:43.931 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法名字:checkLogin
2021-11-15 10:53:43.932 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法注解的title:登錄檢查
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法注解的isSaveRequestData:false
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法的參數(shù):參數(shù)1
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 切入方法的參數(shù):參數(shù)2
2021-11-15 10:53:43.933 6665-6665/com.vincent.aop.project E/aop_tag >>>: 檢測(cè)到未登錄!
4.總結(jié):
通過(guò)上面的例子我們可以發(fā)現(xiàn),AOP解決方案可以使用很簡(jiǎn)潔的代碼實(shí)現(xiàn)我們想要的效果,而且對(duì)原有代碼毫無(wú)入侵性,理解這一原理,可以在工作中更好的以架構(gòu)師的思維去編寫代碼。