AOP實現Android集中式登入架構

xiasem發表於2018-08-16

未經同意禁止抄襲,如需轉載請在顯要位置標註

前言

登入應該是應用開發中一個很常見的功能,一般在應用中有兩種登入,一種是一進入應用就必須登入才能使用(如微信和QQ等),另一種是需要登入的時候才會去登入(如淘寶京東等)。我在工作中遇到的大部分是第二種情況,針對於第二種的登入,我之前都是通過if(){}else()去判斷是否登入的,但是這樣專案結構龐大了之後就會使程式碼臃腫。因為判斷使用者登入狀態是一個頻次很高的操作,所以針對這方面我就考慮有沒有一種方案既能很方便的判斷登入狀態又使程式碼很簡潔。

想來想去方案有兩種,一種是hook到AMS攔截startActivity中的intent,在啟動activity的時候判斷是否登入,如果沒有對intent做動態替換,另一種就是通過AOP實現方法新增判斷登入程式碼片段。hook對系統有相容性,需要考慮到各個版本的api是否改動,而aop的實現方式與版本沒有任何相容性問題,所以最後就採用了aop的方式去實現app集中式登入。

集中式登入架構的使用

為什麼我先講架構的使用,是因為你只有知道了使用這種架構是多麼方便,才會有興趣去了解如何實現這種架構。好了,先來用demo給大家演示一下!

demo效果

看完gif後,你是不是覺得這不就是一個很簡單的demo,通過判斷登陸狀態跳轉不同的頁面嘛,有什麼難的啊!demo是很簡單,但你繼續往下看程式碼,就會覺著這個程式碼實現是多麼酷了!下面看程式碼:
我們在Application裡進行初始化(初始化之後才能接收登入事件,所以越早越好)。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        LoginSDK.getInstance().init(this, new ILogin() {
            @Override
            public void login(Context applicationContext, int userDefine) {
                switch (userDefine) {
                    case 0:
                        startActivity(new Intent(applicationContext, LoginActivity.class));
                        break;
                    case 1:
                        Toast.makeText(applicationContext, "您還沒有登入,請登入後執行", Toast.LENGTH_SHORT).show();
                        break;
                    case 2:
                        new AlertDialog.Builder(MyApplication.this)...
                        break;
                    default:
                        Toast.makeText(applicationContext, "執行失敗,因為您還沒有登入!", Toast.LENGTH_SHORT).show();
                        break;
                }
            }

            @Override
            public boolean isLogin(Context applicationContext) {
                return SharePreferenceUtil.getBooleanSp(SharePreferenceUtil.IS_LOGIN, applicationContext);
            }
        });
    }
  }
複製程式碼

可以看到初始化方法實現了ILogin介面,ILogin介面有兩個方法,第一個login()用於接收登入事件,第二個方法isLogin是判斷登入狀態,這兩個方法留給使用者自己實現,提高架構的可用性。我們所有的登入請求都會回撥到ILogin介面,這也意味著登入事件只有一個統一的入口,這也就是我們集中式登入架構的核心好處了。

好了,我們先來使用以下。

例子1:
//demo演示1 跳轉到需要過濾登入的Activity
@LoginFilter(userDefine = 0)
public void onClick(View view) {
    startActivity(new Intent(this, SecondActivity.class));
}
複製程式碼

上面程式碼就是監聽一個Button的點選事件,然後加入註解@LoginFilter,看方法實現只是跳轉到SecondActivity,並沒有登入邏輯的判斷,但通過這個註解我們就可以在執行時檢測是否登入,如果沒有登入就會中斷方法的執行,轉而呼叫MyApplication裡init()方法中我們自己實現的login()方法,login(Context applicationContext, int userDefine)方法中userDefine是留給使用者自定義的一個值,為了區別使用哪種登入方式。是不是很簡單?再來看例子二:

例子2:

如果我們嫌棄在需要判斷登入狀態的按鈕上加入@LoginFilter()註解麻煩,而是想實現啟動一個Activity自動判斷是否登入,如果沒有登入就回撥到我們的ILogin介面,那麼你只需要建立一個LoginFilterActivity如下:

//demo演示2 直接過濾登陸,不需要加註解,則繼承LoginFilterActivity
public class LoginFilterActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!LoginAssistant.getInstance().getiLogin().isLogin(getApplicationContext())) {
            //TODO: 你可以想做什麼就做什麼,在這裡我讓頁面結束,並給使用者提示
            Toast.makeText(this, "沒有登入!", Toast.LENGTH_SHORT).show();
            finish();
        }
    }

}
複製程式碼

然後我們讓需要登入才能進入的Activity繼承自LoginFilterActivity就可以了。假如UserActivity繼承了LoginFilterActivity,當使用者沒有登入的時候,我們啟動UserActivity的時候便會回撥到我們的ILogin介面,是不是很方便,這就是我們今天要講的集中式登入架構。

下面,我們來講一講如何實現這個架構。

AOP原理

我們先來了解一下AOP,因為這個架構是基於AOP程式設計實現的。

  • 什麼是AOP

關於AOP是什麼,這裡我簡單介紹一下,AOP是Aspect Oriented Programming的縮寫,即面向切面程式設計,與物件導向程式設計(oop)是兩種不同的思維方式,也可以看做是對oop的一種補充。傳統的oop開發會提倡功能模組化等,而aop適合於針對某一型別的問題統一處理。AOP思想的講解不是我們本篇文章的重點,如果有同學對AOP思想不是很理解,這裡我推薦一篇文章,講得很不錯Java AOP & Spring AOP 原理和實現

  • AspectJ介紹

AspectJ是一個面向切面程式設計的一個框架,它擴充套件了java語言,並定義了實現AOP的語法。我們知道,在將.java檔案編譯為.class檔案時預設使用javac編譯工具,而AspectJ會有一套符合java位元組碼編碼規範的編譯工具來替代javac,在將.java檔案編譯為.class檔案時,會動態的插入一些程式碼來做到對某一類特定東西的統一處理。我舉個例子,比如在應用中有很多個button的onClick事件需要檢測是否登入,如果沒有登入則需要去登入之後才能繼續執行,針對這一型別的問題,相對笨一點的做法就是在每一個onClick方法中都顯式的去判斷登入狀態,這樣不免過於麻煩。而我們用AOP的方式實現的話,就需要在每一個onClick方法上加入一個標註,讓編譯器在編譯時能識別到這個標註,然後根據標註來生成一些程式碼檢測登入狀態。好了,如果有同學對AOP還不是很理解的話也不用急,下面我會用例子來給大家演示如何使用AOP實現統一的集中式登入。

AOP實現集中式登入

  • aspectj環境搭建

首先,我們匯入AspectJ的jar包,AspectJ的jar網上一搜就有,也可以直接去我demo裡面拿,LoginArchitecture AOP實現集中式登入 github連結點我。demo裡jar包匯入: AOP實現Android集中式登入架構
好了,匯入jar後還需要在app.gradle配置如下:

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.8'
        classpath 'org.aspectj:aspectjweaver:1.8.8'
    }
}
複製程式碼

然後在檔案末尾新增如下程式碼:

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
//標註1
final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    //標註2
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }
    //標註3
    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);
        //標註4
        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;
            }
        }
    }
}
複製程式碼

關於上面這一大片程式碼就是對aspectj的配置,先看標註1,獲取log列印工具和構建配置,然後標註2判斷是否debug,如果打release把return去掉就可以,標註3處意思是使aspectj配置生效,標註4就是為了在編譯時列印資訊如警告、error等等,這些東西在網上也有很多,大家如果不理解,可以去搜尋一下,這裡不再詳細解釋。

  • 切面程式碼編寫

好了,配置完上面的內容之後,我們就開始編寫程式碼了,首先,定義一個註解LoginFilter,用來註解方法,以便在編譯期被編譯器檢測到需要做切面的方法。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoginFilter {

    int userDefine() default 0;

}
複製程式碼

大家看到我在註解里加了個userDefine,就是為了給使用者提供自定義實現,如根據userDifine值不同做不同的登入處理。

然後,編寫LoginSDK檔案用於初始化和接收登入事件,程式碼如下:

public class LoginSDK {

  public void init(Context context, ILogin iLogin) {
      applicationContext = context.getApplicationContext();
      LoginAssistant.getInstance().setApplicationContext(context);
      LoginAssistant.getInstance().setiLogin(iLogin);
  }

  //...

}
複製程式碼

然後,新建LoginFilterAspect.java檔案用來處理加入LoginFilter註解的方法,對這些方法做統一的切面處理。

@Aspect
public class LoginFilterAspect {
    private static final String TAG = "LoginFilterAspect";

    @Pointcut("execution(@com.xsm.loginarchitecture.lib_login.annotation.LoginFilter * *(..))")
    public void loginFilter() {}

    @Around("loginFilter()")
    public void aroundLoginPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        //標註1
        ILogin iLogin = LoginAssistant.getInstance().getiLogin();
        if (iLogin == null) {
            throw new NoInitException("LoginSDK 沒有初始化!");
        }

        //標註2
        Signature signature = joinPoint.getSignature();
        if (!(signature instanceof MethodSignature)) {
            throw new AnnotationException("LoginFilter 註解只能用於方法上");
        }
        MethodSignature methodSignature = (MethodSignature) signature;
        LoginFilter loginFilter = methodSignature.getMethod().getAnnotation(LoginFilter.class);
        if (loginFilter == null) {
            return;
        }

        Context param = LoginAssistant.getInstance().getApplicationContext();
        //標註3
        if (iLogin.isLogin(param)) {
            joinPoint.proceed();
        } else {
            iLogin.login(param, loginFilter.userDefine());
        }
    }
}

複製程式碼

程式碼並不多,我們來一一解釋。首先看loginFilter方法,這個方法上加入@Pointcut註解,並指定了LoginFilter註解的路徑,@Pointcut註解包括aroundLoginPoint()方法上的@Around註解等都是AspectJ定義的API。@Pointcut註解代表切入點,具體就是指哪些方法需要被執行"AOP"。execution()裡指定了LoginFilter註解的路徑,即加入LoginFilter註解的方法就是需要處理的切面。@Around註解表示這個方法執行時機的前後都可以做切面處理,常用到的還有@Before、@After等等。@Before即方法執行前做處理,@After反之。
好了,aroundLoginPoint(ProceedingJoinPoint joinPoint)方法就是對切面的具體實現了,這裡ProceedingJoinPoint引數意為環繞通知,這個類裡面可以獲取到方法的簽名等各種資訊。

標註1

首先看標註1處,我們先獲取使用者實現的ILogin類,如果沒有呼叫init()設定初始化就丟擲異常。

標註2

標註2處先得到方法的簽名methodSignature,然後得到@LoginFilter註解,如果註解為空,就不再往下走。

標註3

然後看標註3,呼叫iLogin的isLogin()方法判斷是否登入,這個isLogin是留給使用者自己實現的,如果登入,就會繼續執行方法體呼叫方法直到完成,如果沒有登入,呼叫ilogin的login方法,並把userDefine傳過去,login方法是使用者自己實現的。

好了,切面程式碼的處理介紹完了,這個時候我們build一下專案,會在專案下\build\intermediates\classes\debug資料夾生成經過AspectJ編譯器編譯後的.class檔案,我們看下上面例子1中的方法skip(View v)方法,編譯成class檔案的方法體變成了如下這樣:

    @LoginFilter
    public void onClick(View view) {
        JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, view);
        skip_aroundBody1$advice(this, view, var3, LoginFilterAspect.aspectOf(), (ProceedingJoinPoint)var3);
    }
複製程式碼

可以看到我們的點選事件方法已經被植入了一些程式碼,而原來startActivity(new Intent(this, SecondActivity.class));也不見了,實際上這裡是把我們方法的執行給封裝了,這裡會在執行期,目標類載入後,為介面動態生成代理類,將切面織入到代理類中,從而實現對方法進行統一的處理。注:這裡面有個小插曲,就是我在演示的時候

另外,評論中有同學提出單點登入機制處理麻煩,於是我在LoginSDK中加入後臺token驗證失效統一接入入口,我貼出用法:

LoginSDK.getInstance().serverTokenInvalidation(TOKEN_INVALIDATION);
複製程式碼

想要詳細瞭解的同學可以參考demo。

小結

到這裡,是不是覺得通過切面處理登入很簡單,實際上我們只要熟悉了切面程式設計的API,便可以利用這麼簡單的方法對一批擁有某項特徵的東西做特定處理。本專案的demo我放在了github,如果對本篇文章感興趣的同學可以clone下來自己熟悉之後,運用到專案中。demo地址,歡迎star,我的github還有許多有意思的庫,歡迎參觀哦

聯絡方式: xiasem@163.com

相關文章