日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Android面向切面基于AOP實現登錄攔截的場景示例_Android

作者:newki ? 更新時間: 2022-10-16 編程語言

前言

場景如下:用戶第一次下載App,點擊進入首頁列表,點擊個人頁面,需要校驗登錄,然后跳轉到登錄頁面,注冊/登錄完成跳轉到個人頁面。

非常常見的場景,正常我們開發就只能判斷是否已經登錄,如果未登錄就跳轉到登錄,然后登錄完成之后怎么繼續執行?如何封裝?有哪些方式?其實很多人并不清楚。

這里做一個系列總結一下,看看公共有多少種方式實現,你們使用的是哪一種方案,或者說你們覺得哪一種方案最好用。

這一次分享的是全網最多的方案 ,面向切面 AOP 的方式。你去某度一搜,Android攔截登錄 最多的結果就是AOP實現登錄攔截的功能,既然大家都推薦,我們就來看看它到底如何?

一、了解面向切面AOP

我們學習Java的開始,我們一直就知道 OOP 面向對象,其實 AOP 面向切面,是對OOP的一個補充,AOP采取橫向收取機制,取代了傳統縱向繼承體系重復性代碼,把某一類問題集中在一個地方進行處理,比如處理程序中的點擊事件、打印日志等。

AOP是編程思想就是把業務邏輯和橫切問題進行分離,從而達到解耦的目的,提高代碼的重用性和開發效率。OOP的精髓是把功能或問題模塊化,每個模塊處理自己的家務事。但在現實世界中,并不是所有功能都能完美得劃分到模塊中。AOP的目標是把這些功能集中起來,放到一個統一的地方來控制和管理。

我記得我最開始接觸 AOP 還是在JavaEE的框架SSH的學習中,AspectJ框架,開始流行于后端,現在在Android開發的應用中也越來越廣泛了,Android中使用AspectJ框架的應用也有很多,比如點擊事件防抖,埋點,權限申請等等不一而足,這里不展開說明,畢竟我們這一期不是專門講AspectJ的應用的。

簡單的說一下AOP的重點概念(摘抄):

  • 前置通知(Before):在目標方法被調用之前調用通知功能。
  • 后置通知(After):在目標方法完成之后調用通知,此時不會關心方法的輸出是什么。
  • 返回通知(After-returning):在目標方法成功執行之后調用通知。
  • 異常通知(After-throwing):在目標方法拋出異常后調用通知。
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之后執行自定義的行為。
  • 連接點:是在應用執行過程中能夠插入切面的一個點。
  • 切點: 切點定義了切面在何處要織入的一個或者多個連接點。
  • 切面:是通知和切點的結合。通知和切點共同定義了切面的全部內容。
  • 引入:引入允許我們向現有類添加新方法或屬性。
  • 織入:是把切面應用到目標對象,并創建新的代理對象的過程。切面在指定的連接點被織入到目標對象中。在目標對象的生命周期中有多個點可以進行織入:
  • 編譯期: 在目標類編譯時,切面被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
  • 類加載期:切面在目標加載到JVM時被織入。這種方式需要特殊的類加載器(class loader)它可以在目標類被引入應用之前增強該目標類的字節碼。
  • 運行期: 切面在應用運行到某個時刻時被織入。一般情況下,在織入切面時,AOP容器會為目標對象動態地創建一個代理對象。SpringAOP就是以這種方式織入切面的。

簡單理解就是把一個方法拿出來,在這個方法執行前,執行后,做一些特別的操作。關于AOP的基本使用推薦大家看看大佬的教程:

深入理解Android之AOP

不多BB,我們直接看看Android中如何使用AspectJ實現AOP邏輯,實現攔截登錄的功能。

二、集成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打印工具和構建配置
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項目中集成成功。只要集成成功了,使用到是簡單了。

三、定義注解實現功能

定義標記的注解

//不需要回調的處理
@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() {
    }
     //不帶回調的注解處理
    @Around("Login()")
    public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        YYLogUtils.w("走進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;
        //判斷當前是否已經登錄
        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>()
    }
}

其實邏輯很簡單,就是判斷是否登錄,看是放行還是跳轉到登錄頁面

使用的邏輯也是很簡單,把需要處理的邏輯使用方法抽取,并標記注解即可

    override fun init() {
        mBtnCleanToken.click {
            SP().remove(Constants.KEY_TOKEN)
            toast("清除成功")
        }
        mBtnProfile.click {
           //不帶回調的登錄方式
           gotoProfilePage2()
        }
    }
    @Login
    private fun gotoProfilePage2() {
        gotoActivity<ProfileDemoActivity>()
    }

效果:

這..這和我使用Token自己手動判斷有什么區別,完成登錄之后還得我再點一次按鈕,當然了這只是登錄攔截,我想要的是登錄成功之后繼續之前的操作,怎么辦?

其實使用AOP的方式的話,我們可以使用消息通知的方式,比如LiveBus FlowBus之類的間接實現這個效果。

我們先單獨的定義一個注解

//需要回調的處理用來觸發用戶登錄成功后的后續操作
@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() {
    }
    //帶回調的注解處理
    @Around("LoginCallback()")
    public void loginCallbackJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        YYLogUtils.w("走進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;
        //判斷當前是否已經登錄
        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();
        }
    }
    //不帶回調的注解處理
    @Around("Login()")
    public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        YYLogUtils.w("走進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;
        //判斷當前是否已經登錄
        if (LoginManager.isLogin()) {
            joinPoint.proceed();
        } else {
            //如果未登錄,去登錄頁面
            LoginManager.gotoLoginPage();
        }
    }
}

在去登錄頁面之前注冊一個LiveEventBus事件,當登錄完成之后發出通知,這里就直接放行調用注解的方法。即可完成繼續執行的操作。

使用:

    override fun init() {
        mBtnCleanToken.click {
            SP().remove(Constants.KEY_TOKEN)
            toast("清除成功")
        }
        mBtnProfile.click {
           //不帶回調的登錄方式
           gotoProfilePage()
        }
    }
    @LoginCallback
    private fun gotoProfilePage() {
        gotoActivity<ProfileDemoActivity>()
    }

效果:

總結

從上面的代碼我們就基于AOP思想實現了登錄攔截功能,以后我們對于需要用戶登錄之后才能使用的功能只需要在對應的方法上添加指定的注解即可完成邏輯,徹底擺脫傳統耗時耗力的開發方式。

需要注意的是AOP框架雖然使用起來很方便,能幫我們輕松完成函數插樁功能,但是它也有自己的缺點。

AspectJ 在實現時會包裝自己的一些類,不僅會影響切點方法的性能,還會導致安裝包體積的增大。

最關鍵的是對Kotlin不友好,對高版本AGP不友好,所以大家在使用時需要仔細權衡是否適合自己的項目。如有需求可以運行源碼查看效果。源碼在此!

原文鏈接:https://juejin.cn/post/7132643283083198501

欄目分類
最近更新