Android面向切面基于AOP實現(xiàn)登錄攔截的場景示例
前言
場景如下:用戶第一次下載App,點擊進(jìn)入首頁列表,點擊個人頁面,需要校驗登錄,然后跳轉(zhuǎn)到登錄頁面,注冊/登錄完成跳轉(zhuǎn)到個人頁面。
非常常見的場景,正常我們開發(fā)就只能判斷是否已經(jīng)登錄,如果未登錄就跳轉(zhuǎn)到登錄,然后登錄完成之后怎么繼續(xù)執(zhí)行?如何封裝?有哪些方式?其實很多人并不清楚。
這里做一個系列總結(jié)一下,看看公共有多少種方式實現(xiàn),你們使用的是哪一種方案,或者說你們覺得哪一種方案最好用。
這一次分享的是全網(wǎng)最多的方案 ,面向切面 AOP 的方式。你去某度一搜,Android攔截登錄 最多的結(jié)果就是AOP實現(xiàn)登錄攔截的功能,既然大家都推薦,我們就來看看它到底如何?
一、了解面向切面AOP
我們學(xué)習(xí)Java的開始,我們一直就知道 OOP 面向?qū)ο?,其?AOP 面向切面,是對OOP的一個補(bǔ)充,AOP采取橫向收取機(jī)制,取代了傳統(tǒng)縱向繼承體系重復(fù)性代碼,把某一類問題集中在一個地方進(jìn)行處理,比如處理程序中的點擊事件、打印日志等。
AOP是編程思想就是把業(yè)務(wù)邏輯和橫切問題進(jìn)行分離,從而達(dá)到解耦的目的,提高代碼的重用性和開發(fā)效率。OOP的精髓是把功能或問題模塊化,每個模塊處理自己的家務(wù)事。但在現(xiàn)實世界中,并不是所有功能都能完美得劃分到模塊中。AOP的目標(biāo)是把這些功能集中起來,放到一個統(tǒng)一的地方來控制和管理。
我記得我最開始接觸 AOP 還是在JavaEE的框架SSH的學(xué)習(xí)中,AspectJ框架,開始流行于后端,現(xiàn)在在Android開發(fā)的應(yīng)用中也越來越廣泛了,Android中使用AspectJ框架的應(yīng)用也有很多,比如點擊事件防抖,埋點,權(quán)限申請等等不一而足,這里不展開說明,畢竟我們這一期不是專門講AspectJ的應(yīng)用的。
簡單的說一下AOP的重點概念(摘抄):
- 前置通知(Before):在目標(biāo)方法被調(diào)用之前調(diào)用通知功能。
- 后置通知(After):在目標(biāo)方法完成之后調(diào)用通知,此時不會關(guān)心方法的輸出是什么。
- 返回通知(After-returning):在目標(biāo)方法成功執(zhí)行之后調(diào)用通知。
- 異常通知(After-throwing):在目標(biāo)方法拋出異常后調(diào)用通知。
- 環(huán)繞通知(Around):通知包裹了被通知的方法,在被通知的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義的行為。
- 連接點:是在應(yīng)用執(zhí)行過程中能夠插入切面的一個點。
- 切點: 切點定義了切面在何處要織入的一個或者多個連接點。
- 切面:是通知和切點的結(jié)合。通知和切點共同定義了切面的全部內(nèi)容。
- 引入:引入允許我們向現(xiàn)有類添加新方法或?qū)傩浴?/li>
- 織入:是把切面應(yīng)用到目標(biāo)對象,并創(chuàng)建新的代理對象的過程。切面在指定的連接點被織入到目標(biāo)對象中。在目標(biāo)對象的生命周期中有多個點可以進(jìn)行織入:
- 編譯期: 在目標(biāo)類編譯時,切面被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
- 類加載期:切面在目標(biāo)加載到JVM時被織入。這種方式需要特殊的類加載器(class loader)它可以在目標(biāo)類被引入應(yīng)用之前增強(qiáng)該目標(biāo)類的字節(jié)碼。
- 運行期: 切面在應(yīng)用運行到某個時刻時被織入。一般情況下,在織入切面時,AOP容器會為目標(biāo)對象動態(tài)地創(chuàng)建一個代理對象。SpringAOP就是以這種方式織入切面的。
簡單理解就是把一個方法拿出來,在這個方法執(zhí)行前,執(zhí)行后,做一些特別的操作。關(guān)于AOP的基本使用推薦大家看看大佬的教程:
不多BB,我們直接看看Android中如何使用AspectJ實現(xiàn)AOP邏輯,實現(xiàn)攔截登錄的功能。
二、集成AOP框架
Java項目集成
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
組件build.gradle
dependencies {
implementation 'org.aspectj:aspectjrt:1.9.6'
}
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
// 獲取log打印工具和構(gòu)建配置
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
// 判斷是否debug,如果打release把return去掉就可以
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
// return;
}
// 使aspectj配置生效
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);
//在編譯時打印信息如警告、error等等
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;
}
}
}
}
Kotlin項目集成
dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'
項目build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'android-aspectjx'
android {
...
// AOP 配置
aspectjx {
// 排除一些第三方庫的包名(Gson、 LeakCanary 和 AOP 有沖突)
exclude 'androidx', 'com.google', 'com.squareup', 'com.alipay', 'com.taobao',
'org.apache',
'org.jetbrains.kotlin',
"module-info", 'versions.9'
}
}
ependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'org.aspectj:aspectjrt:1.9.5'
}
集成AOP踩坑: zip file is empty
和第三方包有沖突,比如Gson,OkHttp等,需要配置排除一下第三方包,
gradle版本兼容問題
AGP版本4.0以上不支持 推薦使用3.6.1
kotlin兼容問題 :
基本都是推薦使用 com.hujiang.aspectjx
編譯版本兼容問題:
4.0以上使用KT編譯版本為Java11需要改為Java8
組件化兼容問題:
如果在library的moudle中自定義的注解, 想要通過AspectJ來攔截織入, 那么這個@Aspect類必須和自定義的注解在同一moudle中, 否則是沒有效果的
等等...
難點就在集成,如何在指定版本的Gradle,Kotlin項目中集成成功。只要集成成功了,使用到是簡單了。
三、定義注解實現(xiàn)功能
定義標(biāo)記的注解
//不需要回調(diào)的處理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
定義處理類
@Aspect
public class LoginAspect {
@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")
public void Login() {
}
//不帶回調(diào)的注解處理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走進(jìn)AOP方法-Login()");
Signature signature = joinPoint.getSignature();
if (!(signature instanceof MethodSignature)){
throw new RuntimeException("該注解只能用于方法上");
}
Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login == null) return;
//判斷當(dāng)前是否已經(jīng)登錄
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
//如果未登錄,去登錄頁面
LoginManager.gotoLoginPage();
}
}
object LoginManager {
@JvmStatic
fun isLogin(): Boolean {
val token = SP().getString(Constants.KEY_TOKEN, "")
YYLogUtils.w("LoginManager-token:$token")
val checkEmpty = token.checkEmpty()
return !checkEmpty
}
@JvmStatic
fun gotoLoginPage() {
commContext().gotoActivity<LoginDemoActivity>()
}
}
其實邏輯很簡單,就是判斷是否登錄,看是放行還是跳轉(zhuǎn)到登錄頁面
使用的邏輯也是很簡單,把需要處理的邏輯使用方法抽取,并標(biāo)記注解即可
override fun init() {
mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}
mBtnProfile.click {
//不帶回調(diào)的登錄方式
gotoProfilePage2()
}
}
@Login
private fun gotoProfilePage2() {
gotoActivity<ProfileDemoActivity>()
}
效果:

這..這和我使用Token自己手動判斷有什么區(qū)別,完成登錄之后還得我再點一次按鈕,當(dāng)然了這只是登錄攔截,我想要的是登錄成功之后繼續(xù)之前的操作,怎么辦?
其實使用AOP的方式的話,我們可以使用消息通知的方式,比如LiveBus FlowBus之類的間接實現(xiàn)這個效果。
我們先單獨的定義一個注解
//需要回調(diào)的處理用來觸發(fā)用戶登錄成功后的后續(xù)操作
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCallback {
}
修改定義的切面類
@Aspect
public class LoginAspect {
@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")
public void Login() {
}
@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.LoginCallback)")
public void LoginCallback() {
}
//帶回調(diào)的注解處理
@Around("LoginCallback()")
public void loginCallbackJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走進(jìn)AOP方法-LoginCallback()");
Signature signature = joinPoint.getSignature();
if (!(signature instanceof MethodSignature)){
throw new RuntimeException("該注解只能用于方法上");
}
LoginCallback loginCallback = ((MethodSignature) signature).getMethod().getAnnotation(LoginCallback.class);
if (loginCallback == null) return;
//判斷當(dāng)前是否已經(jīng)登錄
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
LifecycleOwner lifecycleOwner = (LifecycleOwner) joinPoint.getTarget();
LiveEventBus.get("login").observe(lifecycleOwner, new Observer<Object>() {
@Override
public void onChanged(Object integer) {
try {
joinPoint.proceed();
LiveEventBus.get("login").removeObserver(this);
} catch (Throwable throwable) {
throwable.printStackTrace();
LiveEventBus.get("login").removeObserver(this);
}
}
});
LoginManager.gotoLoginPage();
}
}
//不帶回調(diào)的注解處理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走進(jìn)AOP方法-Login()");
Signature signature = joinPoint.getSignature();
if (!(signature instanceof MethodSignature)){
throw new RuntimeException("該注解只能用于方法上");
}
Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login == null) return;
//判斷當(dāng)前是否已經(jīng)登錄
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
//如果未登錄,去登錄頁面
LoginManager.gotoLoginPage();
}
}
}
在去登錄頁面之前注冊一個LiveEventBus事件,當(dāng)?shù)卿浲瓿芍蟀l(fā)出通知,這里就直接放行調(diào)用注解的方法。即可完成繼續(xù)執(zhí)行的操作。
使用:
override fun init() {
mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}
mBtnProfile.click {
//不帶回調(diào)的登錄方式
gotoProfilePage()
}
}
@LoginCallback
private fun gotoProfilePage() {
gotoActivity<ProfileDemoActivity>()
}
效果:

總結(jié)
從上面的代碼我們就基于AOP思想實現(xiàn)了登錄攔截功能,以后我們對于需要用戶登錄之后才能使用的功能只需要在對應(yīng)的方法上添加指定的注解即可完成邏輯,徹底擺脫傳統(tǒng)耗時耗力的開發(fā)方式。
需要注意的是AOP框架雖然使用起來很方便,能幫我們輕松完成函數(shù)插樁功能,但是它也有自己的缺點。
AspectJ 在實現(xiàn)時會包裝自己的一些類,不僅會影響切點方法的性能,還會導(dǎo)致安裝包體積的增大。
最關(guān)鍵的是對Kotlin不友好,對高版本AGP不友好,所以大家在使用時需要仔細(xì)權(quán)衡是否適合自己的項目。如有需求可以運行源碼查看效果。源碼在此!
以上就是Android面向切面基于AOP實現(xiàn)登錄攔截的場景示例的詳細(xì)內(nèi)容,更多關(guān)于Android AOP登錄攔截的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android應(yīng)用程序保持后臺喚醒(使用WakeLock實現(xiàn))
本篇文章主要介紹了使用WakeLock使Android應(yīng)用程序保持后臺喚醒的相關(guān)知識。具有很好的參考價值。下面跟著小編一起來看下吧2017-04-04
Android獲取WebView加載url的請求錯誤碼 【推薦】
這篇文章主要介紹了Android獲取WebView加載url的請求錯誤碼 ,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-06-06
Flutter使用Provider進(jìn)行狀態(tài)管理的實現(xiàn)
Provider是Flutter中一個非常流行的狀態(tài)管理工具,它可以幫助開發(fā)者更有效地管理Widget樹中的數(shù)據(jù),本文主要介紹了Flutter使用Provider進(jìn)行狀態(tài)管理的實現(xiàn),感興趣的可以了解一下2024-04-04
Android AAPT(Android Asset Packaging Too
AAPT - Android Asset Packaging Tool作用AAPT基本命令A(yù)APT編譯資源源碼解析AAPT打包和系統(tǒng)不一致的資源2024-04-04
詳解Android平臺JSON預(yù)覽(JSON-handle)
這篇文章主要介紹了Android平臺JSON預(yù)覽(JSON-handle),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09
Android 6.0動態(tài)權(quán)限及跳轉(zhuǎn)GPS設(shè)置界面的方法
今天小編就為大家分享一篇Android 6.0動態(tài)權(quán)限及跳轉(zhuǎn)GPS設(shè)置界面的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-07-07

