AOP實現Android集中式登陸架構

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

前言

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

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

集中式登陸架構的使用

為什么我先講架構的使用,是因為你只有知道了使用這種架構是多么方便,才會有興趣去了解如何實現這種架構。下面看代碼:

我們在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:
@LoginFilter(userDefine = 0)
public void skip(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如下:

public class LoginFilterActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (lib_login_filter_onCreate(true)) {
            //TOOD: 你可以做想做的邏輯,如跳轉到登錄界面或給用戶提示
            finish();
        }
    }

    @LoginFilter
    public Boolean lib_login_filter_onCreate(Boolean aspectParam) { return aspectParam; }
}

然后我們讓需要登陸才能進入的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包導入:
<img src="https://user-gold-cdn.xitu.io/2018/8/16/1654195c6dc3a787?w=433&h=134&f=jpeg&s=6306" />

好了,導入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

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;
            }
        }
    }
}

這一大片代碼就是為了在編譯時打印信息如警告、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 {
            //標注4
            Object target = joinPoint.getTarget();
            Method method = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
            String name = method.getName();
            if (name.contains("lib_login_filter_onCreate")) {
                //標注5
                Object[] args = joinPoint.getArgs();
                if (args != null && args.length == 1 && (args[0] instanceof Boolean)) {
                    joinPoint.proceed(new Object[] {true});
                } else {
                    iLogin.login(param, loginFilter.userDefine());
                }
            } 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是留給使用者自己實現的,如果登陸,就會繼續執行方法體調用方法直到完成,如果沒有登錄,執行標注4。

標注4

首先獲取到方法的對象,通過對象獲取到方法名,然后判斷方法名是否是“lib_login_filter_onCreate”,如果不是,調用iLogin.login()方法,這個login()方法也是留給用戶自己實現的,如果方法名是“lib_login_filter_onCreate”,那么久執行標注5。

標注5

我們還記得在LoginFilterActivity里面有如下方法:

public class LoginFilterActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (lib_login_filter_onCreate(true)) {
            //TOOD: 你可以做想做的邏輯,如跳轉到登錄界面或給用戶提示
            finish();
        }
    }

    @LoginFilter
    public Boolean lib_login_filter_onCreate(Boolean aspectParam) { return aspectParam; }
}

這個lib_login_filter_onCreate方法參數是Boolean類型,并且直接把參數當作返回值返回。其實這個方法的調用就是在標注5處,判斷方法名等于lib_login_filter_onCreate并且參數為Boolean類型的時候,會調用這個方法然后傳入true。那么為何要這么做呢?是因為當我們在繼承LoginFilterActivity的時候,需要自動檢測是否登陸,如果沒有登錄就finish()掉啟動的Activity,所以你也就知道了,這個lib_login_filter_onCreate(Boolean aspectParam)方法是不能隨便亂改的,如果需要進行修改,也要同時對LoginFilterAspect進行修改。

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

    @LoginFilter
    public void skip(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));也不見了,實際上這里是把我們方法的執行給封裝了,這里會在運行期,目標類加載后,為接口動態生成代理類,將切面織入到代理類中,從而實現對方法進行統一的處理。

小結

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

聯系方式:
xiasem@163.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容