未經贊成禁止抄襲,如需轉載請在顯要位置標註
java
登陸應該是應用開發中一個很常見的功能,通常在應用中有兩種登陸,一種是一進入應用就必須登陸才能使用(如微信和QQ等),另外一種是須要登陸的時候纔會去登陸(如淘寶京東等)。我在工做中遇到的大部分是第二種狀況,針對於第二種的登陸,我以前都是經過if(){}else()去判斷是否登陸的,可是這樣項目結構龐大了以後就會使代碼臃腫。由於判斷用戶登陸狀態是一個頻次很高的操做,因此針對這方面我就考慮有沒有一種方案既能很方便的判斷登陸狀態又使代碼很簡潔。
android
想來想去方案有兩種,一種是hook到AMS攔截startActivity中的intent,在啓動activity的時候判斷是否登陸,若是沒有對intent作動態替換,另外一種就是經過AOP實現方法添加判斷登陸代碼片斷。hook對系統有兼容性,須要考慮到各個版本的api是否改動,而aop的實現方式與版本沒有任何兼容性問題,因此最後就採用了aop的方式去實現app集中式登陸。git
爲何我先講架構的使用,是由於你只有知道了使用這種架構是多麼方便,纔會有興趣去了解如何實現這種架構。好了,先來用demo給你們演示一下!github
看完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接口,這也意味着登陸事件只有一個統一的入口,這也就是咱們集中式登陸架構的核心好處了。
api
好了,咱們先來使用如下。bash
//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是留給用戶自定義的一個值,爲了區別使用哪一種登陸方式。是否是很簡單?再來看例子二:微信
若是咱們嫌棄在須要判斷登陸狀態的按鈕上加入@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接口,是否是很方便,這就是咱們今天要講的集中式登陸架構。
app
下面,咱們來說一講如何實現這個架構。
咱們先來了解一下AOP,由於這個架構是基於AOP編程實現的。
關於AOP是什麼,這裏我簡單介紹一下,AOP是Aspect Oriented Programming的縮寫,即面向切面編程,與面向對象編程(oop)是兩種不一樣的思惟方式,也能夠看作是對oop的一種補充。傳統的oop開發會提倡功能模塊化等,而aop適合於針對某一類型的問題統一處理。AOP思想的講解不是咱們本篇文章的重點,若是有同窗對AOP思想不是很理解,這裏我推薦一篇文章,講得很不錯Java AOP & Spring AOP 原理和實現
AspectJ是一個面向切面編程的一個框架,它擴展了java語言,並定義了實現AOP的語法。咱們知道,在將.java文件編譯爲.class文件時默認使用javac編譯工具,而AspectJ會有一套符合java字節碼編碼規範的編譯工具來替代javac,在將.java文件編譯爲.class文件時,會動態的插入一些代碼來作到對某一類特定東西的統一處理。我舉個例子,好比在應用中有不少個button的onClick事件須要檢測是否登陸,若是沒有登陸則須要去登陸以後才能繼續執行,針對這一類型的問題,相對笨一點的作法就是在每個onClick方法中都顯式的去判斷登陸狀態,這樣難免過於麻煩。而咱們用AOP的方式實現的話,就須要在每個onClick方法上加入一個標註,讓編譯器在編譯時能識別到這個標註,而後根據標註來生成一些代碼檢測登陸狀態。好了,若是有同窗對AOP還不是很理解的話也不用急,下面我會用例子來給你們演示如何使用AOP實現統一的集中式登陸。
首先,咱們導入AspectJ的jar包,AspectJ的jar網上一搜就有,也能夠直接去我demo裏面拿,LoginArchitecture AOP實現集中式登陸 github連接點我。demo裏jar包導入:
好了,導入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處,咱們先獲取用戶實現的ILogin類,若是沒有調用init()設置初始化就拋出異常。
標註2處先獲得方法的簽名methodSignature,而後獲得@LoginFilter註解,若是註解爲空,就再也不往下走。
而後看標註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