未经同意禁止抄袭,如需转载请在显要位置标注
前言
登录应该是应用开发中一个很常见的功能,一般在应用中有两种登录,一种是一进入应用就必须登录才能使用(如微信和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 (!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包导入:
好了,导入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