利用AOP实现的更高端的Android集中式登录
Posted 郭霖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用AOP实现的更高端的Android集中式登录相关的知识,希望对你有一定的参考价值。
近日,随着中国电信发布上半年财报,三大运营商上半年“成绩单”全部出炉。财报显示,上半年,中国移动净利润656.41亿元,同比增长4.7%;中国联通净利润25.8亿元,同比增长231.8%;中国电信净利润135.7亿元,同比增长8.1%。经计算,三大运营商上半年总净利817.91亿元,计算下来,平均日赚约4.5亿元。
本篇来自 xiasem 的投稿,分享了利用 AOP 实现的 android 集中式登录架构。一起来看看!希望大家喜欢。
https://juejin.im/user/583d8f7467f356006bbaa7e4
登录应该是应用开发中一个很常见的功能,一般在应用中有两种登录,一种是一进入应用就必须登录才能使用(如微信和 QQ 等),另一种是需要登录的时候才会去登录(如淘宝京东等)。我在工作中遇到的大部分是第二种情况,针对于第二种的登录,我之前都是通过if(){}else()去判断是否登录的,但是这样项目结构庞大了之后就会使代码臃肿。因为判断用户登录状态是一个频次很高的操作,所以针对这方面我就考虑有没有一种方案既能很方便的判断登录状态又使代码很简洁。
想来想去方案有两种,一种是 hook 到 AMS 拦截 startActivity 中的 intent,在启动activity 的时候判断是否登录,如果没有对 intent 做动态替换,另一种就是通过 AOP 实现方法添加判断登录代码片段。hook 对系统有兼容性,需要考虑到各个版本的 api 是否改动,而 aop 的实现方式与版本没有任何兼容性问题,所以最后就采用了 aop 的方式去实现 app 集中式登录。
集中式登录架构的使用
为什么我先讲架构的使用,是因为你只有知道了使用这种架构是多么方便,才会有兴趣去了解如何实现这种架构。好了,先来用 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 (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 链接:
https://github.com/Xiasm/LoginArchitecture
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 {
//标注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 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。
https://github.com/Xiasm/LoginArchitecture
欢迎长按下图 -> 识别图中二维码
以上是关于利用AOP实现的更高端的Android集中式登录的主要内容,如果未能解决你的问题,请参考以下文章
AOP 面向切面编程Android Studio 中配置 AspectJ ( 下载并配置AS中 jar 包 | 配置 Gradle 和 Gradle 插件版本 | 配置 Gradle 构建脚本 )(代