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

學(xué)無(wú)先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語(yǔ)言 正文

Android面向切面基于AOP實(shí)現(xiàn)登錄攔截的場(chǎng)景示例_Android

作者:newki ? 更新時(shí)間: 2022-10-16 編程語(yǔ)言

前言

場(chǎng)景如下:用戶第一次下載App,點(diǎn)擊進(jìn)入首頁(yè)列表,點(diǎn)擊個(gè)人頁(yè)面,需要校驗(yàn)登錄,然后跳轉(zhuǎn)到登錄頁(yè)面,注冊(cè)/登錄完成跳轉(zhuǎn)到個(gè)人頁(yè)面。

非常常見的場(chǎng)景,正常我們開發(fā)就只能判斷是否已經(jīng)登錄,如果未登錄就跳轉(zhuǎn)到登錄,然后登錄完成之后怎么繼續(xù)執(zhí)行?如何封裝?有哪些方式?其實(shí)很多人并不清楚。

這里做一個(gè)系列總結(jié)一下,看看公共有多少種方式實(shí)現(xiàn),你們使用的是哪一種方案,或者說(shuō)你們覺得哪一種方案最好用。

這一次分享的是全網(wǎng)最多的方案 ,面向切面 AOP 的方式。你去某度一搜,Android攔截登錄 最多的結(jié)果就是AOP實(shí)現(xiàn)登錄攔截的功能,既然大家都推薦,我們就來(lái)看看它到底如何?

一、了解面向切面AOP

我們學(xué)習(xí)Java的開始,我們一直就知道 OOP 面向?qū)ο螅鋵?shí) AOP 面向切面,是對(duì)OOP的一個(gè)補(bǔ)充,AOP采取橫向收取機(jī)制,取代了傳統(tǒng)縱向繼承體系重復(fù)性代碼,把某一類問(wèn)題集中在一個(gè)地方進(jìn)行處理,比如處理程序中的點(diǎn)擊事件、打印日志等。

AOP是編程思想就是把業(yè)務(wù)邏輯和橫切問(wèn)題進(jìn)行分離,從而達(dá)到解耦的目的,提高代碼的重用性和開發(fā)效率。OOP的精髓是把功能或問(wèn)題模塊化,每個(gè)模塊處理自己的家務(wù)事。但在現(xiàn)實(shí)世界中,并不是所有功能都能完美得劃分到模塊中。AOP的目標(biāo)是把這些功能集中起來(lái),放到一個(gè)統(tǒng)一的地方來(lái)控制和管理。

我記得我最開始接觸 AOP 還是在JavaEE的框架SSH的學(xué)習(xí)中,AspectJ框架,開始流行于后端,現(xiàn)在在Android開發(fā)的應(yīng)用中也越來(lái)越廣泛了,Android中使用AspectJ框架的應(yīng)用也有很多,比如點(diǎn)擊事件防抖,埋點(diǎn),權(quán)限申請(qǐng)等等不一而足,這里不展開說(shuō)明,畢竟我們這一期不是專門講AspectJ的應(yīng)用的。

簡(jiǎn)單的說(shuō)一下AOP的重點(diǎn)概念(摘抄):

  • 前置通知(Before):在目標(biāo)方法被調(diào)用之前調(diào)用通知功能。
  • 后置通知(After):在目標(biāo)方法完成之后調(diào)用通知,此時(shí)不會(huì)關(guān)心方法的輸出是什么。
  • 返回通知(After-returning):在目標(biāo)方法成功執(zhí)行之后調(diào)用通知。
  • 異常通知(After-throwing):在目標(biāo)方法拋出異常后調(diào)用通知。
  • 環(huán)繞通知(Around):通知包裹了被通知的方法,在被通知的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義的行為。
  • 連接點(diǎn):是在應(yīng)用執(zhí)行過(guò)程中能夠插入切面的一個(gè)點(diǎn)。
  • 切點(diǎn): 切點(diǎn)定義了切面在何處要織入的一個(gè)或者多個(gè)連接點(diǎn)。
  • 切面:是通知和切點(diǎn)的結(jié)合。通知和切點(diǎn)共同定義了切面的全部?jī)?nèi)容。
  • 引入:引入允許我們向現(xiàn)有類添加新方法或?qū)傩浴?/li>
  • 織入:是把切面應(yīng)用到目標(biāo)對(duì)象,并創(chuàng)建新的代理對(duì)象的過(guò)程。切面在指定的連接點(diǎn)被織入到目標(biāo)對(duì)象中。在目標(biāo)對(duì)象的生命周期中有多個(gè)點(diǎn)可以進(jìn)行織入:
  • 編譯期: 在目標(biāo)類編譯時(shí),切面被織入。這種方式需要特殊的編譯器。AspectJ的織入編譯器就是以這種方式織入切面的。
  • 類加載期:切面在目標(biāo)加載到JVM時(shí)被織入。這種方式需要特殊的類加載器(class loader)它可以在目標(biāo)類被引入應(yīng)用之前增強(qiáng)該目標(biāo)類的字節(jié)碼。
  • 運(yùn)行期: 切面在應(yīng)用運(yùn)行到某個(gè)時(shí)刻時(shí)被織入。一般情況下,在織入切面時(shí),AOP容器會(huì)為目標(biāo)對(duì)象動(dòng)態(tài)地創(chuàng)建一個(gè)代理對(duì)象。SpringAOP就是以這種方式織入切面的。

簡(jiǎn)單理解就是把一個(gè)方法拿出來(lái),在這個(gè)方法執(zhí)行前,執(zhí)行后,做一些特別的操作。關(guān)于AOP的基本使用推薦大家看看大佬的教程:

深入理解Android之AOP

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

二、集成AOP框架

Java項(xiàng)目集成

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);
        //在編譯時(shí)打印信息如警告、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項(xiàng)目集成

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'

項(xiàng)目build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'android-aspectjx'
android {
    ...
    // AOP 配置
    aspectjx {
        // 排除一些第三方庫(kù)的包名(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版本兼容問(wèn)題

AGP版本4.0以上不支持 推薦使用3.6.1

kotlin兼容問(wèn)題 :

基本都是推薦使用 com.hujiang.aspectjx

編譯版本兼容問(wèn)題:

4.0以上使用KT編譯版本為Java11需要改為Java8

組件化兼容問(wèn)題:

如果在library的moudle中自定義的注解, 想要通過(guò)AspectJ來(lái)攔截織入, 那么這個(gè)@Aspect類必須和自定義的注解在同一moudle中, 否則是沒(méi)有效果的

等等...

難點(diǎn)就在集成,如何在指定版本的Gradle,Kotlin項(xiàng)目中集成成功。只要集成成功了,使用到是簡(jiǎn)單了。

三、定義注解實(shí)現(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 {
            //如果未登錄,去登錄頁(yè)面
            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>()
    }
}

其實(shí)邏輯很簡(jiǎn)單,就是判斷是否登錄,看是放行還是跳轉(zhuǎn)到登錄頁(yè)面

使用的邏輯也是很簡(jiǎ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自己手動(dòng)判斷有什么區(qū)別,完成登錄之后還得我再點(diǎn)一次按鈕,當(dāng)然了這只是登錄攔截,我想要的是登錄成功之后繼續(xù)之前的操作,怎么辦?

其實(shí)使用AOP的方式的話,我們可以使用消息通知的方式,比如LiveBus FlowBus之類的間接實(shí)現(xiàn)這個(gè)效果。

我們先單獨(dú)的定義一個(gè)注解

//需要回調(diào)的處理用來(lái)觸發(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 {
            //如果未登錄,去登錄頁(yè)面
            LoginManager.gotoLoginPage();
        }
    }
}

在去登錄頁(yè)面之前注冊(cè)一個(gè)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思想實(shí)現(xiàn)了登錄攔截功能,以后我們對(duì)于需要用戶登錄之后才能使用的功能只需要在對(duì)應(yīng)的方法上添加指定的注解即可完成邏輯,徹底擺脫傳統(tǒng)耗時(shí)耗力的開發(fā)方式。

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

AspectJ 在實(shí)現(xiàn)時(shí)會(huì)包裝自己的一些類,不僅會(huì)影響切點(diǎn)方法的性能,還會(huì)導(dǎo)致安裝包體積的增大。

最關(guān)鍵的是對(duì)Kotlin不友好,對(duì)高版本AGP不友好,所以大家在使用時(shí)需要仔細(xì)權(quán)衡是否適合自己的項(xiàng)目。如有需求可以運(yùn)行源碼查看效果。源碼在此!

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

欄目分類
最近更新